View Issue Details

IDProjectCategoryView StatusLast Update
0020274MMW 5Tracklistpublic2023-10-16 17:07
Reporterrusty Assigned To 
PriorityurgentSeveritycrashReproducibilitysometimes
Status closedResolutionfixed 
Product Version5.1 
Target Version5.1Fixed in Version5.1 
Summary0020274: MM crashes on switching between Music views (44A462FE)
DescriptionSwitched from Home to Music [List] to Music [List by Album]. And MM froze.

Crashlog: 44A462FE

This doesn't occur consistently, but I was able to trigger it twice. It seems to be related to the List by Album view (when the bug occurred, prior to freezing, entering the view seemed to trigger and endless amount of activicy in the debug log).
TagsNo tags attached.
Fixed in build2818

Relationships

related to 0020298 closedLudek Artwork and Summary fails to show data when there is only single album in 'List (by Album)' view [regression] 

Activities

Ludek

2023-10-04 13:02

developer   ~0072994

I cannot replicate, but based on the log there must have been incorrect values when drawing the groups in LV,
please generate new debug log with new listview.js
i.e.
replace current
/controls/listview.js
by the attached one and generate new log
listview.js (220,481 bytes)   
'use strict';
registerFileImport('controls/listView');
import { DOM_DELTA_LINE, DOM_DELTA_PAGE, DRAG_DATATYPE } from '../consts';
import Control, { getPixelSize } from './control';
export function setPix(val) {
    if (typeof val === 'number')
        return val + 'px';
    else
        return val;
}
window.setPix = setPix;
const fullLVDebug = true;
const transitionEndEventName = 'webkittransitionend'; // JL: seems like it's supposed to be all lowercase
/**
UI ListView element

@class ListView
@constructor
@extends Control
*/
export default class ListView extends Control {
    _userInteractionDone() {
        // overriden in descendants
    }
    initialize(rootelem, params) {
        super.initialize(rootelem, params);
        let defaultPredrawAmount = 0; //2;
        let defaultDelayBeforePredraw = 400;
        // Public configuration values:
        // Main:
        this.isGrid = false;
        this.isHorizontal = false;
        this.isGrouped = false;
        this.checkGroups = false; // if true, check groups using getGroups and based on the result can be set isGrouped to true or false
        this._showHeader = false;
        this._showInline = false;
        this.reorderOnly = true;
        this._multiselect = true;
        this.itemCloningAllowed = true;
        this.useFastBinding = true;
        this.reportStatus = true;
        this._dynamicSize = false; // This LV has size (height) computed from its content, scrolls with its neighbours (i.e. doesn't operate its own scrollbar)
        this.popupSupport = false; // Show in-place pop-ups for clicked items.
        this.disabledClearingSelection = false; // by default, clicking outside items clears selection
        this.noScroll = (params && params.noScroll) || false; // Prevent scrolling (useful to let mouse wheel messages propagate)
        // Secondary:
        this.distributeEmptySpace = true; // Distribute horizontal space that's normally all on the right side
        this.itemHorzSpacing = 0; // px
        this.itemRowSpacing = 0; // px
        this.groupSpacing = 80; // px   // TODO: More dynamic? 
        this.groupHeaders = true;
        this.groupSeparators = true;
        this.showCaptionOnScroll = false; // Show a large caption when scrolling (currently not working)
        this.moveFirstGroupHeader = true; // Show first header even if it's scrolled off-screen (doesn't work well in Win8, due to scrolling canvas)        
        this.reloadSettings(); // set smooth/animated scroll and gridPopupDelay
        this.smoothScrollTimeLimit = (app.utils.system() === 'macos') ? 0 : (animTools.animationTime || 0.3) * 1000; // ms
        this.focusedAlsoSelected = true; // When focus is moved, the new item is also selected
        this.canScrollHoriz = false; // By default, no horizontal scrollbar is needed
        this._userInteractivePriority = true; // suspend auto-update when user interactive detected
        this._collapseSupport = true;
        this._useMouseHover = false; // we use hvoer only when moving by mouse, not by keys
        // Performance constants
        this.minCachedDivs = 10;
        this.maxCachedDivs = this.minCachedDivs; // Is automatically enlarged in case it's needed in order to cover the whole screen
        this.minTimeBetweenUpdates = 50; // ms
        this.delayBeforeFirstUpdate = 30; // ms
        this.preDrawAmount = defaultPredrawAmount;
        this.delayBeforePredraw = defaultDelayBeforePredraw; // ms
        this.ignoreReflowOptimizations = (params && params.ignoreReflowOptimizations) || false; // JL #18600: Reflow optimizations broke on dropdowns (TODO: Less hacky fix?)
        // The rest of code...
        // this.container.style.overflow = 'hidden';
        this.container.classList.add('listview');
        this.container.setAttribute('role', 'table'); // Screen reader support
        this.createHeaderLayout(); // have to be called before setting passed params, some properties need elements created in these create layout functions
        this.createItemsLayout();
        this.divs = []; // cache all visible item divs
        this.groupDivs = []; // cache all visible group heading divs
        this.groupSepDivs = []; // cache all visible group separator heading divs
        this.skips = []; // array of parts of the listview to be skipped/not drawn (reserved space for pop-ups, etc.)
        this.popupCache = []; // 1-2 cached popups that can be reused for a faster operation
        this._contextMenuPromises = []; // array of promises to wait for in contextMenuHandler
        this.itemCount = 0;
        this.itemHeight = -1;
        this.itemWidth = -1;
        this.itemBoxProperties = {
            height: 0,
            width: 0,
            paddingLeft: 0,
            paddingRight: 0
        };
        this.rowDimension = -1;
        this.colDimension = -1;
        this.smoothScrollAdjust = 0;
        // Groups
        this.groupHeight = -1;
        this.colGroupDimension = -1;
        this.groupSepHeight = -1;
        this.itemsPerRow = -1;
        this.firstCachedItem = 0;
        this.firstVisibleItem = 0;
        this.lastVisibleItem = -1;
        this.lastRefresh = 0;
        this.animateNextDraw = false;
        this.preDraw = false;
        this._shiftFocusedItem = -1; // Item used as an origin for Shift-Click selections
        this.itemRedistSpacing = 0;
        this.preDrawnScreens = 0;
        this._predrawTimeout = undefined;
        this._disablePredraw = false;
        this.drawQueued = false;
        this.focusVisible = false; // After keyboard usage, focus is visible, otherwise not.
        this._parentOffsetHeight = 0; // Set this, so that even non-dynamic height LV can rely on this being '0'.
        this._headerOffsetHeight = 0;
        this.oldWidth = -1;
        this.oldHeight = -1;
        this.canvasScrollLeft = 0;
        this.canvasScrollTop = 0;
        this._containerOffsetTop = 0;
        this._selectionMode = false;
        this.automaticSelectionMode = false;
        this._lassoSelectionStart = undefined;
        this.lassoSelectionEnabled = false;
        this.lassoAutoScrollOffset = 50;
        this.lassoParentElement = true;
        // set passed attributes
        for (let key in params) {
            this[key] = params[key];
        }
        this.updateParentScrollTop(); // LS: this needs to be called after setting of params above so that this.dynamicSize value is correct in scrollingParent getter (in order to get 'showInline' class) 
        //this.enableDragNDrop();
        this.enableTouch();
        if (!this.container.tabIndex || this.container.tabIndex < 0)
            this.container.tabIndex = 0; // Tab index makes sure that we can get focus.
        if (this.horizontalSeparator)
            this.initHorizontalSeparator();
        this.initListeners();
    }
    initHorizontalSeparator() {
        let div = document.createElement('div');
        div.className = 'hSeparatorLine';
        this.container.appendChild(div);
        this.horLineSepDiv = div;
        setVisibilityFast(this.horLineSepDiv, false);
    }
    initListeners() {
        this.localListen(app, 'close', () => {
            // cancel list loading on app close, to avoid unfinished promise error
            if (this._dataSource)
                cancelPromise(this._dataSource.whenLoaded());
        });
        this.localListen(app, 'settingschange', () => {
            this.reloadSettings();
        });
        // prepare mouse event handlers
        // some on viewport, so they are not called when clicking on scrollbar
        this.lastHoveredDiv = undefined;
        this.lastMouseDownDiv = undefined;
        let mouseDownCalled = false;
        app.listen(this.viewport, 'mouseup', (e) => {
            if (e.button == 3 || e.button == 4)
                return; // let the back/forward buttons bubble (#16406)
            if (!this._isTreeView) // #18097
                e.stopPropagation(); // needed when LV is inside of LV (e.g. popups in artist grid)
            // @ts-ignore    
            if (window.getCurrentEditor) // @ts-ignore
                if (window.getCurrentEditor()) // editing in progress ... do not change focus
                    return;
            if (this.lastHoveredDiv) {
                if (this.lastMouseDownDiv === this.lastHoveredDiv) {
                    this.handleItemMouseUp(this.lastHoveredDiv, e); // call mouseup handlers only if mouseup is on the same div as mousedown
                }
                this.setFocus(); // clicked on item ... make LV in focus
            }
            else if (mouseDownCalled && !this.movingOnGroups && !this.isPopupShown() && !e.shiftKey && !e.ctrlKey && !e.altKey &&
                this.dataSource && this.dataSource.clearSelection && e.button == 0 /*primary*/ && !this.disabledClearingSelection) {
                if (!this.showHeader || this.header.offsetHeight < e.offsetY) {
                    let ds = this.dataSource;
                    if (ds) {
                        ds.focusedIndex = -1;
                        ds.modifyAsync(() => {
                            ds.clearSelection();
                            this.selectionMode = false;
                        }, { onlyFlags: true });
                    }
                }
            }
            this._cleanUpLasso();
            this.afterUserInteraction();
            mouseDownCalled = false;
        });
        app.listen(this.viewport, 'mousemove', (e) => {
            if (fullLVDebug)
                ODS('mousemove');
            this._useMouseHover = true;
            this.updateHover(e.clientX, e.clientY);
            if (this.lastHoveredDiv)
                this.handleItemMouseMove(this.lastHoveredDiv, e);
            else
                this.handleLassoMove(null, e);
        });
        app.listen(this.viewport, 'mouseleave', (e) => {
            if (fullLVDebug)
                ODS('mouseleave');
            if (!e.toElement && !e.relatedTarget && window.pageReady) {
                // probably was moved mouse out of the window or
                // #16253: when user click on track, mouseleave with toElement and relatedTarget undefined is called
                if (thisWindow.bounds.mouseInside() && !e.clientX && !e.clientY) // mouse is in window
                    return;
            }
            this.updateHover(-1, -1);
        });
        app.listen(this.viewport, 'mouseover', (e) => {
            if (fullLVDebug)
                ODS('mouseover');
            this.updateHover(e.clientX, e.clientY);
            if (this.lastHoveredDiv)
                this.handleItemMouseOver(this.lastHoveredDiv, e);
        }, true);
        app.listen(this.viewport, 'mousedown', (e) => {
            this._useMouseHover = true;
            if (e.button == 3 || e.button == 4)
                return; // let the back/forward buttons bubble (#16406)
            if (this.lastHoveredDiv) {
                this.lastMouseDownDiv = this.lastHoveredDiv;
                this.redrawFocusedItem(false);
                this.handleItemMouseDown(this.lastHoveredDiv, e);
                this.movingOnGroups = false;
            }
            else {
                this.lastMouseDownDiv = undefined;
                this.handleLassoStart(null, e);
            }
            mouseDownCalled = true; // indication, that mousedown was called on this LV
            e.stopPropagation();
            // @ts-ignore
            window._lastLVMouseDownTm = Date.now();
            this.afterUserInteraction();
        }, false);
        app.listen(this.viewport, 'click', (e) => {
            if (this.lastMouseDownDiv) {
                this.handleItemClick(this.lastMouseDownDiv, e);
            }
            e.stopPropagation();
            this.afterUserInteraction();
        }, false);
        app.listen(this.viewport, 'dblclick', (e) => {
            if (this.lastHoveredDiv) {
                this.handleItemDblClick(this.lastHoveredDiv, e);
            }
            e.stopPropagation();
            this.afterUserInteraction();
        }, false);
        app.listen(this.canvas, 'scroll', this.handleCanvasScroll.bind(this), false);
        app.listen(this.canvas, 'wheel' /* JL: changed from mousewheel to wheel */, this.mouseWheelHandler.bind(this), false);
        app.listen(this.canvas, 'mousedown', this.mousedownHandler.bind(this));
        this.registerEventHandler('keydown');
        this.registerEventHandler('keyup');
        this.registerEventHandler('layoutchange', true);
        this.localListen(window, 'lesschange', () => {
            this.lessChanged();
        });
    }
    lessChanged() {
        this.itemHeightReset = true;
        this._refreshItemBoxProperties = true;
        this._adjustSizeNeeded = true;
        this._groupsRefresh = true;
        this._reComputeViewport = true;
        this.invalidateAll();
    }
    _updateHover_RateLimit(x, y) {
        if (x == -1 && y == -1) {
            if (this.lastHoveredDiv) {
                this.lastHoveredDiv.removeAttribute('data-hover');
                this.lastHoveredDiv = undefined;
            }
            return;
        }
        //if (fullLVDebug)
        //    ODS('_updateHover_RateLimit: x,y: ' + x + ',' + y);
        let rect = this.canvas.getBoundingClientRect();
        this._lastHoverUpdate = Date.now();
        if (this._canvasStartRect === undefined) {
            if (this.scrollingParent) {
                this._canvasStartRect = {
                    top: rect.top + this._parentScrollTop,
                    left: rect.left,
                    width: rect.width,
                    height: rect.height,
                };
            }
            else {
                this._canvasStartRect = this.canvas.getBoundingClientRect();
            }
        }
        let offsetX = x - rect.left;
        let offsetY = y - rect.top;
        if (!this.isGrid && !this.ignoreMouseOnGroup && this.colGroupDimension > 0) {
            offsetX += this.colGroupDimension;
        }
        // JH: The following hover handling is faster, since it doesn't require getBoundingClientRect(), but it is sometimes a bit off,
        //     which occurs when pop-up is being opened and there's also a smooth scrolling performed meanwhile.
        //     TODO: Fix the issues, so that this faster version could be enabled again.
        //            var offsetX = x - this._canvasStartRect.left;
        //            var offsetY = y - this._canvasStartRect.top;
        //            if (this.dynamicSize) {
        //                offsetY += this.getSmoothScrollOffset() + this.container.offsetTop;
        //            } else
        //            if (this.scrollingParent) { // If we have any scrolling element as a parent, use it for the calculation
        //                offsetY += this._parentScrollTop;
        //            }
        //            if (fullLVDebug)
        //                ODS("**Hover " + this.itemCount + ": " + y + ', ' + offsetY + " - " + this._parentScrollTop + ", " + this._canvasStartRect.top);
        let itIdx;
        if ((offsetX >= 0) && (offsetY >= 0) && this._canvasStartRect && (offsetX < this._canvasStartRect.width) && (offsetY < this._canvasStartRect.height) &&
            !(this.oldDropBefore || this.oldDropAfter /* don't draw hover while dragging */)) {
            itIdx = this.getItemFromRelativePosition(offsetX, offsetY);
        }
        let it = undefined;
        if (itIdx >= 0)
            it = this.getDiv(itIdx);
        if (fullLVDebug)
            ODS('_updateHover_RateLimit: ' + it + '|' + this.lastHoveredDiv + ' x,y: ' + x + ',' + y);
        let itChanged = (it !== this.lastHoveredDiv);
        if (itChanged) {
            if (this.lastHoveredDiv) {
                this.lastHoveredDiv.removeAttribute('data-hover');
            }
            if (it) {
                it.setAttribute('data-hover', '1');
            }
            this.raiseEvent('itemhoverchange', {
                lastDiv: this.lastHoveredDiv,
                newDiv: it
            });
            this.lastHoveredDiv = it;
            // @ts-ignore
            window._lastHoveredListViewDiv = it; // used for animations (to zoom from correct rectangle)
        }
    }
    updateHover(x, y) {
        if (!this._useMouseHover) {
            if (this.lastHoveredDiv) { // remove mousehover, so we do not have more if using keyboard, #17844
                this.lastHoveredDiv.removeAttribute('data-hover');
                this.raiseEvent('itemhoverchange', {
                    lastDiv: this.lastHoveredDiv,
                    newDiv: undefined
                });
                this.lastHoveredDiv = undefined;
            }
            return;
        }
        if (x === undefined)
            x = window.mouseX;
        if (y === undefined)
            y = window.mouseY;
        if (fullLVDebug)
            ODS('**updateHover: x,y: ' + x + ',' + y);
        // Rate limiting implemented to decrease CPU utilization (#12956)
        const diff = Date.now() - (this._lastHoverUpdate || 0);
        this.requestTimeout(() => {
            ODS('Timeout Process');
            this._updateHover_RateLimit(x, y);
        }, Math.max(0, 18 - diff), '_updateHoverTimeout');
    }
    mouseWheelHandler(e) {
        this._useMouseHover = true;
        if (this._dynamicSize || this.noScroll || (window.isMenuVisible && window.isMenuVisible()))
            return;
        if (e.stopPropagation)
            e.stopPropagation();
        this.redrawFocusedItem(false);
        if (e.ctrlKey || e.altKey) // Alt key is here for Chromium which currently doesn't send Ctrl+Wheel events, since they are reserved for whole HTML page zoom (to be manually implemented by us)
         {
            if (e.wheelDelta > 0)
                this.zoomIn();
            else
                this.zoomOut();
        }
        else {
            let horz = this.isHorizontal;
            let delta = e.wheelDelta;
            if ((!horz && e.wheelDeltaX) || (e.shiftKey && e.wheelDeltaY)) {
                // scroll left-right on non horizontal view
                if (e.shiftKey) {
                    this.canvas.scrollLeft = this.canvas.scrollLeft + (-e.wheelDeltaY);
                    this.afterUserInteraction();
                    return;
                }
                else {
                    this.canvas.scrollLeft = this.canvas.scrollLeft + (-e.wheelDeltaX);
                }
            }
            if (!horz) {
                delta = -e.deltaY;
                if (e.deltaMode === DOM_DELTA_LINE) {
                    delta *= this.itemHeight;
                }
                else if (e.deltaMode === DOM_DELTA_PAGE) { // #16342
                    delta *= this.container.clientHeight;
                }
            }
            let scroll = this.getScrollOffset();
            let newPos = scroll - delta;
            /*                var scrollAnimationEnded = function() {
                                scrollCounter--;
                                if(!scrollCounter)
                                    app.unlisten(this.canvas, transitionEndEventName, scrollAnimationEnded);
                                updateHover();
                            };
                            if(!scrollCounter)
                                app.listen(this.canvas, transitionEndEventName, scrollAnimationEnded);
                            scrollCounter++;*/
            this.setSmoothScrollOffset(newPos);
            /*if (this._lastOffset === undefined) {
                this._lastOffset = 0;
                this._gumStartTime = Date.now();
            }
            if (Date.now() - this._gumStartTime < 350) {
                var neg = delta < 0 ? -1 : 1;
                if (Math.abs(delta) > 200)
                    delta = neg * 200;
                this._lastOffset = this._lastOffset + (delta / Math.max(1, ((Date.now() - this._gumStartTime) / 20)));
                if (Math.abs(this._lastOffset) > 300 * 8)
                    this._lastOffset = neg * 300 * 8;
                this._setGum(false, scroll - this._lastOffset);
                if (this._gumTimer) {
                    this.smoothScrollTime = this.smoothScrollTimeLimit;
                }
            }*/
        }
        this.afterUserInteraction();
    }
    mousedownHandler() {
        this.redrawFocusedItem(false);
    }
    cancelDrop() {
        this.updateDropEffect(undefined);
        if (this.autoScrollInt) {
            clearInterval(this.autoScrollInt);
            this.autoScrollInt = undefined;
        }
    }
    doAutoScrollStep() {
        this.setScrollOffset(this.getScrollOffset() + this.autoScrollStep);
        let srcitem = this.lastMouseDragEvent.dataTransfer.getUserData('itemindex');
        let item = this.getDropIndex(this.lastMouseDragEvent);
        if (dnd.isSameControl(this.lastMouseDragEvent) && (item == srcitem || item == srcitem + 1))
            this.updateDropEffect(undefined);
        else
            this.updateDropEffect(item);
    }
    createDiv() {
        let _this = this;
        let div;
        if (this.itemCloningAllowed && this.divs[0]) {
            div = this.divs[0].cloneNode(true);
            // have to remove possible hovered flag, it is not re-set during data binding
            div.removeAttribute('data-hover');
            div.cloned = true;
            if (!this.divs[0].isVis) {
                div.style.display = '';
                div.isVis = true;
            }
        }
        else {
            div = document.createElement('div');
            div.className = 'lvItem';
            if (this.isGrid)
                div.classList.add('griditem');
            else
                div.classList.add('rowitem');
            div.style.position = 'absolute';
            div.setAttribute('role', 'row'); // Screen reader support
        }
        div.parentListView = this;
        app.listen(div, 'touchstart', function (e) {
            if (e.touches.length == 1) {
                div._touchPos = e.touches[0];
            }
        }, window.addPassiveOption(false));
        app.listen(div, 'touchend', function (e) {
            if (e.changedTouches.length == 1 && div._touchPos) {
                let touch = e.changedTouches[0];
                if (Math.abs(div._touchPos.clientX - touch.clientX) < 5 && Math.abs(div._touchPos.clientY - touch.clientY) < 5) {
                    if (_this.longTouch(e)) {
                        _this.handleItemLongTouch(this, e);
                    }
                }
            }
        }.bind(div), window.addPassiveOption(false));
        if (this.dndEventsRegistered)
            this.makeDraggable(div);
        this.addItemToCanvas(div);
        this.setUpDiv(div);
        precompileBinding(div, this);
        // set initial state of inner divs
        this.resizeDiv(div, this.oldWidth, this.oldHeight);
        if (this.disabled) {
            // set initial disabled state, do this only when (disabled = true) otherwise disabledCounter would get incorrect value
            div.setAttribute('data-disabled', 1);
            this.setChildsDisabled(div, true, true);
        }
        return div;
    }
    createGroupDiv() {
        let div = document.createElement('div');
        div.className = 'groupHeader';
        div.parentListView = this;
        div.style.position = 'absolute';
        this.setUpGroupHeader(div);
        this.addItemToCanvas(div);
        return div;
    }
    createGroupSepDiv() {
        let div = document.createElement('div');
        div.className = 'groupSepHeader';
        div.parentListView = this;
        div.style.position = 'absolute';
        this.setUpGroupSep(div);
        this.addItemToCanvas(div);
        return div;
    }
    deleteDiv(itemIndex) {
        if (itemIndex >= this.firstCachedItem && itemIndex < this.firstCachedItem + this.divs.length) { // this item in in cache
            let offset = itemIndex - this.firstCachedItem;
            let div = this.divs[offset];
            this.divs.splice(offset, 1); // remove from cache
            this.cancelItemLoadingPromise(div);
            this.cleanUpDiv(div);
            return div;
        }
        else
            return null; // no change
    }
    /**
    Returns the div at the corresponding item index, or null if no div contains the item.

    @method getDiv
    @param integer Index of list
    @return HTMLElement|null Div at the corresponding item index if it exists.
    */
    getDiv(itemIndex) {
        if (itemIndex >= this.firstCachedItem && itemIndex < this.firstCachedItem + this.divs.length) { // this item in in cache
            return this.divs[itemIndex - this.firstCachedItem];
        }
        else
            return null;
    }
    // returns either a new div or a div found in the cache
    getDivFromCache(firstitem, itemindex) {
        let offset = itemindex - this.firstCachedItem;
        if (offset < 0) { // Cache has to be shifted
            let newFirstItem = itemindex;
            let newoffset = this.firstCachedItem - newFirstItem;
            if (newoffset < this.maxCachedDivs) { // It makes sense to move some divs (otherwise it doesn't, the indexes are far off).
                let saveItems = Math.min(this.divs.length - newoffset, this.divs.length);
                let moveItems = this.divs.length - saveItems;
                this.divs = this.divs.slice(-moveItems)
                    .concat(new Array(Math.max(0, newoffset - moveItems)), this.divs.slice(0, saveItems));
            }
            this.firstCachedItem = newFirstItem;
            offset = itemindex - this.firstCachedItem;
        }
        else if (offset < this.divs.length) { // The div is within our cache
            // No need to do anything
        }
        else { // The div has to be added
            if (offset >= this.maxCachedDivs) { // Cache has to be shifted
                let newFirstItem = Math.min(Math.max(itemindex - Math.round(this.maxCachedDivs / 2), 0), firstitem);
                let newoffset = newFirstItem - this.firstCachedItem;
                if (newoffset < this.divs.length) {
                    this.divs = this.divs.slice(newoffset).concat(this.divs.slice(0, newoffset));
                }
                this.firstCachedItem = newFirstItem;
                offset = itemindex - this.firstCachedItem;
            }
        }
        if (offset >= this.divs.length)
            this.divs.length = offset + 1;
        let div = this.divs[offset];
        if (!div) {
            div = this.createDiv();
            this.divs[offset] = div;
        }
        return div;
    }
    beforeDraw() {
        if (this.isGrid)
            this._oldSize = this.viewport.getBoundingClientRect();
    }
    afterDraw() {
        if (this.isGrid) {
            this.requestTimeout(() => {
                let newSize = this.viewport.getBoundingClientRect();
                if ((this._oldSize.right - this._oldSize.left !== newSize.right - newSize.left) ||
                    (this._oldSize.bottom - this._oldSize.top !== newSize.bottom - newSize.top)) {
                    this.adjustSize(true);
                    this.invalidateAll();
                }
            }, 100, 'afterDrawCheck');
        }
    }
    hideAllDivs() {
        this.divs.forEach(function (div) {
            this.hideDiv(div);
        }.bind(this));
    }
    hideDiv(div) {
        if (div.isVis || div.isVis === undefined) {
            if (div.isMoving) {
                this.cancelTransition(div, 'data-moving');
                div.isMoving = false;
            }
            div.style.display = 'none'; // #17776 JL: Fixing extreme layout recalcs after loading large lvPopups
            div.isVis = false;
            if (this.suspendDiv(div))
                div.forceRebind = true;
        }
    }
    hideGroupCollapseMark(div) {
        if (div._collapseMark)
            div._collapseMark.style.top = setPix(-2 * this.groupHeight - 60);
    }
    hideGroupDiv(div) {
        this.cancelItemLoadingPromise(div);
        div.style.top = setPix(-2 * this.groupHeight - 60); // Move away to not be visible (but not too far, so that it moves in fast during animations)
        this.hideGroupCollapseMark(div);
        div.groupid = undefined; // #18981
    }
    hideGroupSepDiv(div) {
        div.style.top = setPix(-2 * this.groupHeight - 60); // Move away to not be visible (but not too far, so that it moves in fast during animations)
    }
    setUpTransition(div, attribute, finishCallback) {
        let transitionFinished = function () {
            if (finishCallback)
                finishCallback();
            this.removeAttribute(attribute);
            app.unlisten(this, transitionEndEventName, transitionFinished);
            if (attribute == 'data-moving')
                this.isMoving = false;
        }.bind(div);
        if (!div.hasAttribute(attribute)) {
            app.listen(div, transitionEndEventName, transitionFinished);
            div.setAttribute(attribute, '1');
        }
    }
    cancelTransition(div, attribute) {
        div.removeAttribute(attribute);
        // eslint-disable-next-line no-self-assign
        div.style.top = div.style.top;
        // eslint-disable-next-line no-self-assign
        div.style.left = div.style.left;
    }
    setMinHeight(value) {
        this.container.style.minHeight = value;
        let valInt = parseInt(value);
        let valWithoutHeader = valInt - this.header.offsetHeight;
        if (valWithoutHeader < 0)
            valWithoutHeader = 0;
        this.canvas.style.minHeight = valWithoutHeader + 'px';
        this.viewport.style.minHeight = valWithoutHeader.toString();
    }
    createPopupIndicator() {
        if (!this.popupIndicator) {
            let ind = document.createElement('div');
            ind.className = 'popupIndicator';
            ind.style.position = 'absolute';
            ind.style.pointerEvents = 'none';
            ind.style.zIndex = '10000';
            loadIconFast('popupIndicator', function (icon) {
                ind.appendChild(icon);
            });
            this.addItemToCanvas(ind);
            this.popupIndicator = ind;
        }
        return this.popupIndicator;
    }
    draw_groups(scrollTop) {
        let h = this.getVisibleRowsDim();
        let regroupRequired = false;
        let renderOffsetTop = 0;
        if (this.dynamicSize)
            renderOffsetTop = scrollTop;
        let igroup = 0;
        let group = this.getOffsetGroup(scrollTop);
        if (group) {
            let rgd = 0;
            if (!this.moveFirstGroupHeader)
                rgd = group.rowGroupDimension || 0;
            if (rgd < 0)
                rgd = 0;
            if (group) {
                let doneDivs = [];
                for (; group.offset < scrollTop + h; igroup++) {
                    if (fullLVDebug)
                        ODS('** LV.draw_groups: group.offset=' + group.offset + ', scrollTop: ' + scrollTop + ', h: ' + h + ', igroup = ' + igroup);
                    let nextGroup = this.getNextGroup(group);
                    if (!nextGroup)
                        break;
                    let lastGroup = (group.id == nextGroup.id);
                    let offset = group.offset;
                    let groupStart = offset - scrollTop + renderOffsetTop;
                    if (this.groupHeaders) {
                        // Try to find an already rendered group header
                        let index = this.groupDivs.findIndex((e) => (e.groupid == group.id));
                        let div;
                        if (index >= 0)
                            div = this.groupDivs.splice(index, 1)[0];
                        else {
                            div = this.groupDivs.pop();
                            if (!div)
                                div = this.createGroupDiv();
                        }
                        doneDivs.push(div);
                        if (div.groupid !== group.id || div.forceInvalidate) {
                            // Render side group header
                            if (div.groupid !== group.id)
                                this.cancelItemLoadingPromise(div);
                            this.renderGroupHeader(div, group, (div.groupid !== group.id)); // force rebind only if group changed, to avoid flickering
                            div.groupid = group.id;
                        }
                        div.forceInvalidate = undefined;
                        this.hideGroupCollapseMark(div);
                        let oldGroupStart = groupStart;
                        if ((group.rowGroupDimension === undefined) || (group.colGroupDimension <= 0)) {
                            let gw = div.clientWidth;
                            let gh = div.clientHeight;
                            if (this.isHorizontal) {
                                group.rowGroupDimension = gw;
                                group.colGroupDimension = gh;
                            }
                            else {
                                group.rowGroupDimension = gh;
                                group.colGroupDimension = gw;
                            }
                            requestAnimationFrame(() => {
                                // This method must be called outside of read lock (otherwise it can cause deadlock when recompute groups is in progress)
                                if (this._dataSource)
                                    this._dataSource.setGroupDimension(group.groupid, group.rowGroupDimension, group.colGroupDimension);
                            });
                            if (this.colGroupDimension < group.colGroupDimension)
                                this.colGroupDimension = group.colGroupDimension;
                            if (this.groupHeight < gh) {
                                this.groupHeight = gh;
                                regroupRequired = true; // minimal group height was changed
                            }
                        }
                        if (this.moveFirstGroupHeader) {
                            if (offset < scrollTop) { // Try to show the group header on screen
                                div.setAttribute('data-partial', 1);
                                let groupEnd;
                                if (lastGroup)
                                    groupEnd = this.getViewportSize();
                                else
                                    groupEnd = nextGroup.offset - this.groupSpacing;
                                if (scrollTop + group.rowGroupDimension <= groupEnd)
                                    offset = scrollTop; // We can fit the group header fully
                                else
                                    offset = groupEnd - group.rowGroupDimension;
                                groupStart = offset + renderOffsetTop - scrollTop; // set new groupStart show group header
                            }
                            else
                                div.removeAttribute('data-partial');
                        }
                        // Move div to the correct position
                        if (this.isHorizontal) {
                            div.style.left = setPix(groupStart);
                            div.style.top = 0;
                        }
                        else {
                            div.style.top = setPix(groupStart);
                            div.style.left = 0;
                        }
                        if (this.renderGroupHeaderPartial) {
                            this.renderGroupHeaderPartial(div, group, groupStart - oldGroupStart);
                        }
                    }
                    // Render group separator
                    if (this.groupSeparators) {
                        let lDiv;
                        if (igroup < this.groupSepDivs.length)
                            lDiv = this.groupSepDivs[igroup];
                        else {
                            lDiv = this.createGroupSepDiv();
                            this.groupSepDivs.push(lDiv);
                        }
                        this.renderGroupSep(lDiv, group);
                        lDiv.style.top = setPix(groupStart - this.groupSepHeight);
                        lDiv.style.left = '0';
                        lDiv.style.height = this.groupSepHeight.toString();
                        lDiv.style.width = this.colDimension.toString();
                    }
                    if (lastGroup) {
                        igroup++;
                        break; // The last group
                    }
                    group = nextGroup;
                }
                this.groupDivs = doneDivs.concat(this.groupDivs);
            }
        }
        for (let i = igroup; i < this.groupDivs.length; i++) {
            this.hideGroupDiv(this.groupDivs[i]);
        }
        for (let i = igroup; i < this.groupSepDivs.length; i++) {
            this.hideGroupSepDiv(this.groupSepDivs[i]);
        }
        if (regroupRequired)
            this.groupsRecompute(false, true /* viewport size compute */, false);
    }
    draw_locked() {
        if (fullLVDebug)
            ODS('***LV draw_locked() for itemcount: ' + this.itemCount + ', ' + (this.visible ? 'visible' : 'hidden') + ', uniqueId = ' + this.uniqueID);
        let startDrawTm = Date.now();
        this.beforeDraw();
        if (this.recalcLayoutNeeded)
            this.recalcLayout();
        if (this._adjustSizeNeeded)
            this.adjustSize();
        if (this._restoreScrollPos) {
            this._restoreScrollPos = undefined;
            this.restoreRealScroll();
        }
        let _this = this;
        let visibleRect = this.getVisibleRect(); // has to be _after_ recalcLayout()
        visibleRect.width = this.canvasWidth; // this differs when scrollingParent is defined
        if (fullLVDebug)
            ODS('***LV draw_locked() rect: ' + visibleRect.top + ', height: ' + visibleRect.height + ', uniqueId = ' + this.uniqueID);
        if (this._predrawTimeout) {
            clearTimeout(this._predrawTimeout);
            this._predrawTimeout = null;
        }
        let animate = false;
        if (this.animateNextDraw) {
            animate = true;
            this.animateNextDraw = false;
        }
        if (!this.preDraw)
            this.renderState('itemsLoading');
        if (((this.itemHeight <= 0) || (this.itemHeightReset)) && !this.dynamicSize) { // JH: TODO: there's already adjustSize() above, should be united?
            let origscroll = 0;
            let size = this.getViewportSize();
            if (size > 0) {
                origscroll = this.getScrollOffset() / size;
                this.adjustSize(true);
                // Scroll to the same position as previously (as much as possible)
                this.setScrollOffset(origscroll * this.getViewportSize());
            }
        }
        let h, w;
        if (this.isHorizontal) {
            h = visibleRect.width;
            w = visibleRect.height;
        }
        else {
            h = visibleRect.height;
            w = visibleRect.width;
        }
        if (this.forceCanvasHeight >= 0)
            h = this.forceCanvasHeight;
        //let rowSpacing = this.itemRowSpacing;
        let scrollTop = Math.round(visibleRect.top);
        let scrollTopOrig = scrollTop;
        if (this.preDraw)
            scrollTop = Math.max(0, scrollTop - (1 + this.preDrawnScreens) * h);
        let renderOffsetTop = 0;
        if (this.dynamicSize)
            renderOffsetTop = scrollTopOrig;
        //let oldFirstVisible = this.firstVisibleItem;
        //let oldLastVisible = this.lastVisibleItem;
        // Get the first visible item
        let firstitem = this.getItemForCanvas(scrollTop, this.colGroupDimension) || 0;
        let offset_row;
        if (this.dynamicSize) {
            offset_row = Math.max(Math.min(-this._parentOffsetHeight, -this.itemHeight - ((this.popupDiv && this.isPopupShown()) ? this.getPopupHeight(this.popupDiv) : 0)), this.getItemTopOffset(firstitem) - scrollTopOrig); // need to start before zero enough, otherwise it does not compute correctly. #17213
        }
        else {
            offset_row = this.getItemTopOffset(firstitem) - scrollTopOrig;
        }
        let group = this.getItemGroup(firstitem);
        if (!this.preDraw || this.forceRebindAll) {
            if (this.forceRebindAll) { // rebind all groups when forceRebindAll is true
                this.groupDivs.forEach(function (div) {
                    div.groupid = null;
                    this.hideGroupCollapseMark(div);
                }.bind(this));
            }
            this.draw_groups(scrollTop);
        }
        let offset_col = this.colGroupDimension;
        if (this.forceRebindAll) {
            this.forceRebindAll = false;
            this.forceRebindSelection = false;
            this.divs.forEach(function (div) {
                div.forceRebind = true;
            });
        }
        else {
            if (this.forceRebindSelection) {
                this.forceRebindSelection = false;
                this.divs.forEach(function (div) {
                    div.rebindSelection = true;
                });
            }
        }
        this.firstVisibleItem = firstitem;
        /*
        if (this.showCaptionOnScroll && !this.preDraw) {
            if (!this.scrollingCaption) {
                var div = document.createElement('div');
                div.parentListView = this;
                div.style.position = 'absolute';
                div.style.zIndex = '99999';
                div.style.color = 'white';
                div.style.textAlign = 'center';
                div.style.width = '100%';
                //div.style.top='50%';

                div.style.maxHeight = '100%';
                div.style.maxWidth = '100%';
                div.style.top = '0px';
                div.style.bottom = '0px';
                div.style.left = '0px';
                div.style.right = '0px';
                div.style.margin = 'auto';

                //		        this.addItemToCanvas(div);
                this.container.appendChild(div);
                this.scrollingCaption = div;
            }
            //			if (!WINDOWS_METRO || !this.isHorizontal)
            //				this.scrollingCaption.style.top = setPix( renderOffsetTop + Math.floor(size.h*0.3));
            //			else
            //				this.scrollingCaption.style.top = setPix( Math.floor(size.h*0.3));
            this.scrollingCaption.style.fontSize = setPix(Math.floor(size.h / 3));
            this.scrollingCaption.style.height = setPix(Math.floor(size.h / 3));
            //			this.scrollingCaption.style.marginTop = setPix( -Math.floor(size.h * 0.2));
            this.scrollingCaption.innerHTML = firstitem;
        }
        */
        let drawHeight = (this.preDraw ? (2 + this.preDrawnScreens) * h : h);
        // Handle skipping of regions
        let skipAfterIndex;
        let skip;
        let nextSkipIndex = 0;
        let prepareNextSkip = function () {
            if (skip && skip.div) {
                let oldVis = skip.div.style.visibility;
                skip.div.style.visibility = (skip.visible ? '' : 'hidden');
                if (skip.visible && (oldVis !== skip.div.style.visibility) && _this.popupDiv && (_this.popupDiv.parentElement === skip.div)) {
                    _this.requestFrame(function () {
                        if (_this.popupDiv)
                            _this.renderPopup(_this.popupDiv); // re-render popup on visibility change, it is sometimes rendered incorrectly otherwise, e.g. when autoscrolled into view
                    }.bind(_this), 'renderPopup');
                }
            }
            skip = _this.skips[nextSkipIndex++];
            if (skip) {
                skip.visible = false;
                skipAfterIndex = skip.afterIndex;
            }
            else
                skipAfterIndex = Number.MAX_SAFE_INTEGER;
        };
        do
            prepareNextSkip();
        while (skipAfterIndex < firstitem);
        let item;
        let addSkips = function () {
            let addrow = 0;
            while (item >= skipAfterIndex) {
                if (skip.div) {
                    let style = skip.div.style;
                    if (_this.isHorizontal) {
                        style.left = Math.round(renderOffsetTop + offset_row);
                        style.top = Math.round(offset_col);
                    }
                    else {
                        style.top = Math.round(renderOffsetTop + offset_row);
                        style.left = Math.round(offset_col);
                    }
                }
                skip.visible = true;
                addrow = Math.max(skip.reservePx, addrow);
                prepareNextSkip();
            }
            offset_row += addrow;
        };
        //ODS('--- ' + this.container.getAttribute('data-id') + ',  h=' + h + ', _parentOffsetHeight=' + this._parentOffsetHeight + ', _parentScrollTop=' + this._parentScrollTop + ', _containerOffsetTop=' + this._containerOffsetTop + ', _containerOffsetHeight=' + this._containerOffsetHeight + ', _headerOffsetHeight=' + this._headerOffsetHeight);
        if (fullLVDebug || (drawHeight - offset_row > 10000 /* something is bad */))
            ODS('***LV draw_locked() main loop: firstitem: ' + firstitem + ', offset_row: ' + offset_row + ', drawHeight: ' + drawHeight + ', itemCount: ' + this.itemCount + ', uniqueId = ' + this.uniqueID);
        // Main loop
        let itemsDrawn = 0;
        let itemsBound = 0;
        if (drawHeight > 0) {
            for (item = firstitem; item < this.itemCount && offset_row < drawHeight; item++) {
                let div = this.getDivFromCache(firstitem, item);
                if (div.isVis === undefined) {
                    div.isVis = false;
                }
                if (!this.preDraw) {
                    let newLeft;
                    let newTop;
                    if (this.isHorizontal) {
                        newLeft = Math.round(renderOffsetTop + offset_row);
                        newTop = Math.round(offset_col);
                    }
                    else {
                        newTop = Math.round(renderOffsetTop + offset_row);
                        newLeft = Math.round(offset_col);
                    }
                    if (animate && (newTop != parseInt(div.style.top) || newLeft != parseInt(div.style.left))) {
                        // JH: The following doesn't properly animate, since transition start follows immediately and so the values aren't taken into account
                        /*				if (!div.isVis)
                                        {
                                            div.style.left = offset_col;
                                            if (offset_row < h/2)
                                                div.style.top = offset_row - this.itemHeight;
                                            else
                                                div.style.top = offset_row + this.itemHeight;
                                        }*/
                        if (div.isVis) {
                            this.setUpTransition(div, 'data-moving');
                            div.isMoving = true;
                        }
                    }
                    // Move div to the correct position
                    div.style.left = setPix(newLeft);
                    div.style.top = setPix(newTop);
                }
                if (this.isGrid)
                    div.style.width = setPix(this.itemBoxProperties.width);
                else {
                    let reqW = this.requiredWidth(w); // Set the width for the full length of the row (so that e.g. selection is properly drawn if horizontally scrolled).
                    if (!reqW)
                        reqW = w;
                    div.style.width = setPix(reqW - this.colGroupDimension - this.itemBoxProperties.paddingLeft - this.itemBoxProperties.paddingRight);
                }
                div.style.height = setPix(this.itemBoxProperties.height);
                if (!div.isVis) {
                    div.style.display = ''; // #17776 JL: Fixing extreme layout recalcs after loading large lvPopups
                    div.isVis = true;
                }
                if ((div.itemIndex !== item) || (div.forceRebind)) {
                    this.handleBinding_locked(div, item);
                    itemsDrawn++;
                    if (this.preDraw) {
                        this.hideDiv(div);
                    }
                    else
                        this.lastBindTimestamp = Date.now();
                }
                else {
                    if (this._dataSource && div.rebindSelection)
                        this.markSelected(div, this._dataSource.isSelected(item));
                }
                itemsBound++;
                if (this.isGrouped && this._collapseSupport) {
                    // compute next index when group is collapsed
                    let newItemIdx = item + 1;
                    if (group) {
                        if (group.collapsed && (group.visibleTracks !== group.itemCount)) {
                            if ((group.index + group.visibleTracks) - 1 < item + 1)
                                newItemIdx = group.index + group.itemCount;
                        }
                    }
                    else {
                        newItemIdx = Math.min(this.itemCount, this.getNextItemIndex(item));
                    }
                    if (newItemIdx !== item + 1) {
                        // hide divs between old and new index
                        for (let k = item + 1; k < newItemIdx; k++) {
                            let _div = this.getDiv(k);
                            if (_div)
                                this.hideDiv(_div);
                        }
                        item = newItemIdx - 1;
                    }
                }
                // Calculations for the new column/row
                let newGroup = (this.isGrouped && group && (item + 1 >= group.index + group.visibleTracks));
                offset_col += this.colDimension + this.itemHorzSpacing + this.itemRedistSpacing;
                if (offset_col + this.colDimension > w || !this.isGrid || newGroup) { // A new row of items
                    offset_col = this.colGroupDimension;
                    offset_row += this.rowDimension + this.itemRowSpacing;
                    if (this._collapseSupport && newGroup && !this.isGrid && group && group.collapsable) {
                        // show 'expand' text and mark
                        let divM = this.getGroupCollapseMark(group);
                        if (divM) {
                            if (!divM.isVis) {
                                divM.style.display = ''; // #17776 JL: Fixing extreme layout recalcs after loading large lvPopups
                                divM.isVis = true;
                            }
                            divM.style.left = setPix(offset_col);
                            divM.style.top = setPix(offset_row + renderOffsetTop);
                            this.renderCollapseMark(divM, group);
                        }
                    }
                    // Handle skipping of regions
                    addSkips();
                    if (newGroup) {
                        offset_row += Math.max(0, group.rowGroupDimension - this.calcPixsPerItems(group.visibleTracks)); // when items size is less than group size
                        group = this.getItemGroup(item + 1);
                        offset_row += this.groupSpacing - this.itemRowSpacing + this.groupSepHeight;
                    }
                }
            }
        }
        // Add skips that should be below all items
        offset_col = this.colGroupDimension;
        offset_row += this.rowDimension + this.itemRowSpacing;
        addSkips();
        while (skip)
            prepareNextSkip(); // Make sure all the remaining skips are processed (hidden)
        if (!this.preDraw)
            this.lastVisibleItem = item - 1;
        if (!this.preDraw) { // JH: We keep items visible in Win Metro, since IE needs to (slowly) re-render items with moved offset. So we rather keep them where they are.
            for (let idiv = 0; idiv < this.divs.length; idiv++) { // Hide all cached divs that aren't visible
                let divindex = idiv + this.firstCachedItem;
                if (divindex < this.firstVisibleItem || divindex > this.lastVisibleItem) {
                    let div = this.divs[idiv];
                    if (div)
                        this.hideDiv(div);
                }
            }
        }
        if (!this.preDraw && this.preDrawAmount > 0 && !this._disablePredraw && !this._predrawTimeout) {
            this.preDrawnScreens = 0;
            //this.lastBindTimestamp = Date.now();
            let _this = this;
            this._predrawTimeout = _this.requestTimeout(function () {
                _this.requestFrame(function () {
                    _this.preDrawScreen();
                }, 'preDrawScreen');
            }, this.delayBeforePredraw);
        }
        // Handle popup indicator drawing
        let popup = this.getSkip('popup');
        if (popup) {
            // Pop-up indicator
            let divP = this.getDiv(popup.afterIndex);
            if (divP && popup.rendered) {
                this.createPopupIndicator();
                let popstyle = this.popupIndicator.style;
                popstyle.visibility = '';
                popstyle.left = divP.style.left;
                popstyle.top = divP.style.top;
                popstyle.height = this.itemHeight;
                popstyle.width = this.itemWidth;
            }
            else
                popup = undefined;
        }
        if (!popup && this.popupIndicator)
            this.popupIndicator.style.visibility = 'hidden';
        if (!this.preDraw)
            this.renderState('itemsLoaded');
        if (fullLVDebug)
            ODS('***LV draw_locked() finished');
        this.afterDraw();
        let took = Date.now() - startDrawTm;
        if (fullLVDebug || (took > 200)) {
            let details = took + ' ms, items bound: ' + itemsBound + ' ms, items drawn: ' + itemsDrawn + ', drawHeight: ' + drawHeight + ', items count: ' + this.itemCount + ', control: ' + this.container.getAttribute('data-id') + ', ' + this.uniqueID;
            ODS('***LV draw_locked() took: ' + details);
            if (took > 5000)
                assert('Drawing of LV took ' + details);
        }
    }
    drawnow() {
        if (this._isDrawing) { // #19350
            if (fullLVDebug)
                ODS('LV: Skipping drawnow because we are inside a synchronous drawnow call already');
            return;
        }
        if (!this.visible)
            return; // don't draw invisible controls
        if (fullLVDebug)
            ODS('***LV drawnow');
        this._isDrawing = true; // To prevent recursive drawnow calls
        if (this.scrollUpdateNeeded) {
            this.canvasScrollLeft = this.canvas.scrollLeft;
            this.canvasScrollTop = this.canvas.scrollTop;
            if (this._notifiedScrollTop != this.canvasScrollTop) {
                this.invalidateScrollPos = true;
                this._notifiedScrollTop = this.canvasScrollTop;
            }
            this.headerItems.scrollLeft = this.canvasScrollLeft;
            if (!this.isHorizontal) {
                if (this.viewport.scrollLeft !== this.canvasScrollLeft) {
                    this.viewport.scrollLeft = this.canvasScrollLeft;
                    this.invalidateNeeded = true; // horizontal scrolling in gridview -> udpate values
                }
            }
        }
        if (this.invalidateNeeded) {
            this.invalidateNeeded = false;
            if (this._dataSource)
                this.setItemCount(this._dataSource.count);
            else
                this.setItemCount(0);
            this.divs.forEach(function (div) {
                div.itemIndex = undefined;
                div.forceRebind = true;
            });
            this.groupDivs.forEach(function (div) {
                div.forceInvalidate = true;
            });
            if (this._requestScrollPosition) {
                this._itemToShow = undefined; // LS: to supress scheduled scrolling in _setItemFullyVisible()                    
                this.setScrollOffset(this._requestScrollPosition);
                this._requestScrollPosition = undefined;
            }
            if (this._requestFocusIndex !== undefined) {
                if (this._dataSource && (this._dataSource.itemsSelected > 1)) { // we have already something selected, set only focused index, to avoid clearing selection
                    if (this._requestFocusIndex !== this.focusedIndex)
                        this.focusedIndex = this._requestFocusIndex;
                }
                else {
                    if (this._requestFocusIndex !== this.focusedIndex) {
                        let reqFoc = this._requestFocusIndex;
                        this._requestFocusIndex = undefined;
                        this.setFocusedAndSelectedIndex(reqFoc).then(() => {
                            this._requestFocusIndex = reqFoc;
                            this.invalidateAll();
                        });
                        this._isDrawing = false;
                        return;
                    }
                }
                this._requestFocusIndex = undefined;
                if (this.smoothScroll) { // Temporarily disable smoothscroll. Should be done in a cleaner fashion?
                    this.smoothScroll = false;
                    this.smoothScrollOrigin = undefined; // needed, it could lead to deadlock 
                    this.requestTimeout(() => {
                        this.smoothScroll = true;
                    }, 100, 'smoothscrolldisable');
                }
                if (this._requestPopup) {
                    this._requestPopup = undefined;
                    this.showPopup(this.focusedIndex);
                }
            }
        }
        this._setItemFullyVisible(); // In case there's a need to scroll to an item
        let wasSmoothScrollInUse = (typeof this.smoothScrollOrigin != 'undefined');
        enterLayoutLock(this.container); // We need to prevent layout changes notifications during draw operations of the inner part of the listview (TODO: avoid the event _after_ this call??)
        if (this._dataSource) {
            this._dataSource.locked(() => {
                if (this.visible)
                    this.setItemCount(this._dataSource.count); // Make sure we draw the corrent # of items                
                this.draw_locked();
            });
        }
        else
            this.draw_locked();
        leaveLayoutLock(this.container); // Try ... finally was intentionally left out here, since it currently isn't optimized by Chromium
        if (!this.preDraw)
            this.lastRefresh = Date.now();
        if (wasSmoothScrollInUse || this.scrollUpdateNeeded) {
            this.raiseEvent('scroll', {}, true, true); // Notify that our interior was scrolled and content is now rendered according to the scroll value.
            this.updateHover();
            if (wasSmoothScrollInUse && !this.dynamicSize)
                this.draw(); // Schedule a new draw in order to smoothly animate scroll
        }
        this.scrollUpdateNeeded = false;
        this._isDrawing = false;
    }
    deferredDraw() {
        if (fullLVDebug)
            ODS('***LV deferredDraw, invalidateNeeded: ' + this.invalidateNeeded + ', callstack: ' + app.utils.logStackTrace());
        if (window.hasBeenShown) {
            this.requestFrame(() => {
                this.drawnow();
                this.drawQueued = false;
                _applyLayoutQueryCallbacks(); // #18600
                _applyStylingCallbacks();
            }, 'deferredDraw');
            this.drawQueued = true;
        }
        else {
            // If the window is in the process of loading
            this.requestTimeout(() => {
                this.drawnow();
                this.drawQueued = false;
                _applyLayoutQueryCallbacks(); // #18600
                _applyStylingCallbacks();
            }, 1000, 'deferredDraw');
            this.drawQueued = true;
        }
    }
    draw() {
        if (this.smoothScroll) {
            this.deferredDraw();
        }
        else {
            this.drawnow();
        }
    }
    preDrawScreen() {
        this._predrawTimeout = undefined;
        if (this._cleanUpCalled)
            return;
        if (this.preDrawnScreens >= this.preDrawAmount || this.drawQueued)
            return;
        let diff = Date.now() - this.lastBindTimestamp;
        if (diff >= this.delayBeforePredraw) {
            this.preDraw = true;
            this.draw();
            this.preDraw = false;
            this.preDrawnScreens++;
        }
        let _this = this;
        this._predrawTimeout = _this.requestTimeout(function () {
            _this.requestFrame(function () {
                _this.preDrawScreen();
            }, 'preDrawScreen');
        }, 30); // Just a short delay in order to give other JS methods a chance to run (e.g. another draw during scrolling).
    }
    getNextItemIndex(item) {
        return ++item;
    }
    getGroupCollapseMark(group) {
        let createCollapseMarkDiv = () => {
            let div = document.createElement('div');
            div.parentListView = this;
            div.classList.add('collapseRow');
            div.style.position = 'absolute';
            this.localListen(div, 'click', () => {
                if (div.group && this.dataSource && this.dataSource.setCollapsed) {
                    this.dataSource.setCollapsed(div.group.id, !div.group.collapsed);
                    //this.invalidateAll();
                    this.groupsRecompute(false, true, true);
                }
            });
            this.addItemToCanvas(div);
            return div;
        };
        // Try to find an already rendered group header
        let index = this.groupDivs.findIndex((e) => (e.groupid == group.id));
        let div;
        let mark;
        if (index >= 0)
            div = this.groupDivs[index];
        if (div) {
            div._collapseMark = div._collapseMark || createCollapseMarkDiv();
            mark = div._collapseMark;
            mark.group = group;
        }
        return mark;
    }
    renderCollapseMark(div, group) {
        if (group.collapsed) {
            div.innerText = _('Show all') + ' ' + group.itemCount + ' ' + _('track', 'tracks', group.itemCount);
        }
        else {
            div.innerText = _('Collapse');
        }
    }
    notifyControlFocus() {
        this.raiseEvent('focusedcontrol', {
            control: this
        }, false, true /* bubbles */);
        let ds = this.dataSource;
        if (ds && ds.count && isUsingKeyboard()) {
            if (this.focusedIndex < 0 /* #15638 */) {
                this.setFocusedAndSelectedIndex(0).then(() => {
                    this.setFocusedFullyVisible();
                });
            }
            else {
                if (isUsingKeyboard() && (this.focusedIndex >= 0) && this.focusedAlsoSelected)
                    this.setSelectedIndex(this.focusedIndex, true); // make sure, focused item is also selected, #17849 11)              
                this.setFocusedFullyVisible();
            }
        }
    }
    canDrawFocus() {
        return false;
    }
    raiseSelectionChanged() {
        this.raiseEvent('selectionChanged', {
            control: this,
            modeOn: this.selectionMode
        }, false, true /* bubbles */);
    }
    // focus LV without automatic scrolling within parent container
    setFocus() {
        this.container.focus({
            preventScroll: true
        });
    }
    fileTransferPrepare(element, e) {
        if (this.dataSource) {
            let item = this.dataSource.focusedItem;
            let track = null;
            if (item) {
                if (item.objectType === 'track') {
                    track = item;
                }
                else if (item.objectType === 'playlistentry') {
                    track = item.sd;
                }
                if (track) {
                    e.dataTransfer.setData('DownloadURL', this.dataSource.toSeparatedString(true, '*'));
                    e.dataTransfer.setUserData('_localDrop', '1'); // this indicates we're dragging single track inside MM (we need this indicator as dragging files from external app uses same DownloadURL and URL properties)
                }
            }
        }
    }
    canDrop(e) {
        let sameListView = dnd.isSameControl(e); /* by default, allow D&D inside same listview */
        return this.dndEventsRegistered && sameListView;
    }
    dragOver(e) {
        if (e.shiftKey && !this.reorderOnly)
            dnd.setDropMode(e, 'copy');
        let totalPos = this.canvas.getBoundingClientRect();
        let offsetX = e.clientX - totalPos.left;
        let offsetY = e.clientY - totalPos.top;
        this.lastMouseDragEvent = e;
        let item = this.getDropIndex(e);
        if (dnd.headerMoving(e)) {
            if (item) // we cannot drop header to list
                e.dataTransfer.dropEffect = 'none';
            return;
        }
        // Show where the drop is going to happen
        let srcitem = e.dataTransfer.getUserData('itemindex');
        //ODS('DROP: '+dnd.isSameControl(e)+' '+item+"/"+srcitem);
        if (dnd.isSameControl(e) && (item == srcitem || item == srcitem + 1))
            this.updateDropEffect(undefined);
        else
            this.updateDropEffect(item);
        // Automatically scroll if close to borders
        let offsetRow;
        if (this.isHorizontal)
            offsetRow = offsetX;
        else
            offsetRow = offsetY;
        if (this.dynamicSize) {
            offsetRow -= this.getScrollOffset();
        }
        let perc = offsetRow / this.getVisibleRowsDimVirtual();
        let autoStartPerc = 0.20;
        if (perc < autoStartPerc || perc > (1 - autoStartPerc)) {
            if (perc < autoStartPerc)
                perc -= autoStartPerc;
            else
                perc -= (1 - autoStartPerc);
            let _this = this;
            this.autoScrollStep = perc * 500;
            if (!this.autoScrollInt)
                this.autoScrollInt = setInterval(function () {
                    _this.doAutoScrollStep();
                }, 50);
        }
        else {
            if (this.autoScrollInt) {
                clearInterval(this.autoScrollInt);
                this.autoScrollInt = undefined;
            }
        }
    }
    dragFinished(e) {
        this.cancelDrop();
        super.dragFinished(e);
    }
    dragLeave(e) {
        if (!isInElement(e.clientX, e.clientY, this.container)) {
            this.cancelDrop();
        }
    }
    getDropMode(e) {
        if (!dnd.isSameControl(e))
            return 'copy';
        return 'move';
    }
    getDropIndex(e) {
        let pos = 0;
        if (dnd.isDragEvent(e)) {
            let totalPos = this.canvas.getBoundingClientRect();
            let offsetX = e.clientX - totalPos.left;
            let offsetY = e.clientY - totalPos.top;
            pos = this.getItemFromRelativePosition(offsetX, offsetY);
            if (pos === undefined)
                pos = this.itemCount;
            else {
                if (offsetY + this.getSmoothScrollOffset() - this.getItemTopOffset(pos) > this.itemHeight / 2)
                    pos++; // Drop item _behind_ the currently hovered items, in case we are in the lower half of the item.
            }
        }
        else {
            pos = this.dataSource.focusedIndex;
            if (pos < 0)
                pos = 0;
        }
        // @ts-ignore
        if (this.isAllowedDropIndex && !this.isAllowedDropIndex(pos))
            return (this.dataSource.focusedIndex + 1) || 0; // #17294
        else
            return pos;
    }
    drop(e, isSameControl) {
        this.cancelDrop();
        let dropMode = dnd.getDropMode(e);
        if (dropMode == 'move') {
            this.dropToPosition(this.getDropIndex(e));
        }
    }
    setDragElementData(element, e) {
        super.setDragElementData(element, e);
        let selCount = 0;
        if (this.dataSource) {
            selCount = this.dataSource.itemsSelected;
            if (selCount > 1) {
                let cont = dnd.getCustomDragElement(element, selCount);
                e.dataTransfer.setDragImage(cont, e.offsetX, e.offsetY);
            }
        }
        e.dataTransfer.setUserData('datarow', 'datarow');
        e.dataTransfer.setUserData('itemindex', element.itemIndex);
        let dataType = this.getDragDataType();
        if (!dataType && this.dataSource && (element.itemIndex < this.dataSource.count)) {
            this.dataSource.locked(function () {
                this._fastObject = this.dataSource.getFastObject(element.itemIndex, this._fastObject);
                if (this._fastObject.dataSource) {
                    e.dataTransfer.setUserData(DRAG_DATATYPE, this._fastObject.dataSource.objectType);
                }
                else {
                    if (this._fastObject.objectType) {
                        e.dataTransfer.setUserData(DRAG_DATATYPE, this._fastObject.objectType);
                    }
                }
            }.bind(this));
        }
        else {
            e.dataTransfer.setUserData(DRAG_DATATYPE, dataType);
        }
        element.parentListView.setSelectedIndex(element.itemIndex, selCount > 1); // Make sure that the dragged item is also selected
    }
    getDraggedObject(e) {
        let ret = null;
        if (this.dataSource) {
            this.dataSource.locked(function () {
                ret = this.dataSource.getSelectedList();
            }.bind(this));
        }
        return ret;
    }
    resizeDiv(div, w, h) {
        if (div.lastTestedWidth === w)
            return;
        div.lastTestedWidth = w;
        if (this.itemSizes) {
            for (let i = 0; i < this.itemSizes.length; i++) {
                let obj = this.itemSizes[i];
                if ((!obj.fromWidth || (obj.fromWidth <= w)) && (!obj.toWidth || (obj.toWidth > w))) {
                    if (obj.height !== undefined)
                        div.style.height = obj.height + 'px';
                    if (obj.className) {
                        div.classList.toggle(obj.className, true);
                    }
                    this.itemHeightReset = true; // cause reset sizes                
                }
                else {
                    if (obj.className)
                        div.classList.toggle(obj.className, false);
                }
            }
        }
        if (!div.sizeDependentElements)
            return;
        forEach(div.sizeDependentElements, function (el) {
            if (el.limits.fromWidth || el.limits.toWidth) {
                if ((w >= el.limits.fromWidth) && (!el.limits.toWidth || (w < el.limits.toWidth))) {
                    if (!el.hiddenByShowif && !isVisible(el, false)) {
                        setVisibility(el, true, {
                            layoutchange: false
                        });
                        div.forceRebind = true;
                    }
                    el.hiddenBySize = false;
                }
                else {
                    setVisibility(el, false, {
                        layoutchange: false
                    });
                    el.hiddenBySize = true;
                }
            }
            if (el.condWidths) {
                let notSet = true;
                for (let i = 0; i < el.condWidths.length; i++) {
                    let obj = el.condWidths[i];
                    if ((!obj.fromWidth || (obj.fromWidth <= w)) && (!obj.toWidth || (obj.toWidth > w))) {
                        if (obj.width !== undefined) {
                            el.style.width = obj.width;
                            notSet = false;
                        }
                        if (obj.className) {
                            el.classList.toggle(obj.className, true);
                        }
                        break;
                    }
                    else {
                        if (obj.className) {
                            el.classList.toggle(obj.className, false);
                        }
                    }
                }
                if (notSet)
                    el.style.width = ''; // no given fixed width found, set default
            }
        }.bind(this));
    }
    resizeDivs(w, h) {
        if (!this.divs)
            return;
        this.divs.forEach((div) => {
            if (div)
                this.resizeDiv(div, w, h);
        });
    }
    /**
    Returns the top scrolled item information/offset, so that it can be restored in case LV formatting/size is changed (and thus scroll offset of the canvas wouldn't match).

    @method getRealScrollOffset
    @return Object Information about the scrolled position
    */
    getRealScrollOffset() {
        let topItem = this.getItemFromRelativePosition(0, 0, true /*approximate*/);
        let origScroll;
        if (topItem >= 0)
            origScroll = this.getItemTopOffset(topItem) - this.getScrollOffset();
        else
            origScroll = this.getScrollOffset();
        return {
            topItem: topItem,
            origScroll: origScroll
        };
    }
    /**
    Restores the top scrolled item according to the saved position.

    @method setRealScrollOffset
    @param Object Previously saved scroll position (by getRealScrollOffset method)
    */
    setRealScrollOffset(position) {
        let totOffset = this.getItemTopOffset(position.topItem) - position.origScroll;
        this.setScrollOffset(totOffset);
    }
    saveRealScroll() {
        if (this.invalidateScrollPos || !this.savedScrollOffset) {
            this.invalidateScrollPos = false;
            this.savedScrollOffset = this.getRealScrollOffset();
        }
        return this.savedScrollOffset;
    }
    restoreRealScroll(sc) {
        sc = sc || this.savedScrollOffset;
        if (sc && sc.topItem /* it's not empty */) {
            this.setRealScrollOffset(sc);
            this.invalidateScrollPos = false; // This might have changed scroll offset, but not intentionally, so ignore.
            this._notifiedScrollTop = this.canvas.scrollTop;
        }
    }
    recalcLayout(redraw) {
        if (window.hasBeenShown) {
            queryLayoutAfterFrame(() => {
                if (!this._cleanUpCalled)
                    this._recalcLayout(redraw);
            });
        }
        else {
            // If the window is in the process of loading
            this.requestTimeout(() => {
                this._recalcLayout(redraw);
            }, 1000, '_recalcLayout');
        }
    }
    _recalcLayout(redraw) {
        let isVis = this.visible;
        if (fullLVDebug)
            ODS('**** recalcLayout started, item count: ' + this.itemCount + ', ' + (isVis ? 'visible' : 'hidden') + ', uniqueId = ' + this.uniqueID);
        if (isVis) {
            if (this.recalcLayoutNeeded)
                this.oldVisible = false; // To force recalc below
            this.recalcLayoutNeeded = false;
        }
        else {
            this.recalcLayoutNeeded = true;
            return;
        }
        // Keep canvas position cached, so that e.g. mouse hover can be calculated faster    
        this._canvasStartRect = undefined;
        let newWidth = this.container.offsetWidth;
        let newHeight = this.container.offsetHeight;
        let widthChange = (newWidth != this.oldWidth);
        let heightChange = (newHeight != this.oldHeight);
        let anyChange = false;
        let newTop;
        let newLeft;
        if (this.dynamicSize) {
            this.updateParentScrollTop();
            newTop = findScreenPos(this.container).top;
            if (this.scrollingParent)
                newTop -= findScreenPos(this.scrollingParent).top - this.scrollingParent.scrollTop /* always use current scroll position (even in case smooth scroll is in progress) as we need to know exact offset for further header positioning */;
            newLeft = this.container.offsetLeft;
            let parent = this.scrollingParent;
            anyChange = (newLeft != this.oldLeft || newTop != this.oldTop ||
                (parent && this._parentOffsetHeight != parent.offsetHeight));
            if (fullLVDebug)
                ODS('   ** parent: ' + this._parentOffsetHeight + ' vs. ' + parent.offsetHeight + ', self: [' + this.oldLeft + ',' + this.oldTop + '],H:' + this.oldHeight + ' vs. [' + newLeft + ',' + newTop + '],H:' + newHeight);
        }
        let sizeChange = (widthChange || heightChange);
        if (fullLVDebug)
            ODS('**Recalc layout old: ' + this.oldWidth + '/' + this.oldHeight + ', new: ' + newWidth + '/' + newHeight + ', uniqueId: ' + this.uniqueID);
        if (sizeChange || anyChange || !this.oldVisible) {
            if (fullLVDebug)
                ODS('**** recalcLayout sizeChange: ' + sizeChange + ' , anyChange: ' + anyChange + ', oldVisible: ' + this.oldVisible + ', uniqueId: ' + this.uniqueID);
            this.getCanvasSizeAndPos(false /*not cached - to get the current values*/);
            if (!this.dynamicSize) {
                this.saveRealScroll();
            }
            let scrollChanged = this.oldTop !== newTop;
            this.oldWidth = newWidth;
            this.oldHeight = newHeight;
            this.oldLeft = newLeft;
            this.oldTop = newTop;
            this.oldVisible = isVis;
            if (this.dynamicSize && this.scrollingParent) {
                redraw = true;
                // Cache some layout values for faster drawing later
                let parent = this.scrollingParent;
                this._containerOffsetTop = newTop;
                this._containerOffsetHeight = this.container.offsetHeight;
                this._headerOffsetHeight = this.header.offsetHeight;
                this._parentOffsetHeight = parent.offsetHeight;
                deferredNotifyLayoutChangeDown(parent); // #19067
                if (fullLVDebug)
                    ODS('** recalcLayout: top: ' + this._containerOffsetTop + ' , height: ' + this._containerOffsetHeight + ', header.height: ' + this._headerOffsetHeight + ', parent.height: ' + this._parentOffsetHeight + ', uniqueId: ' + this.uniqueID);
            }
            if (sizeChange) {
                this.adjustSize(true);
                redraw = true;
            }
            if (!this.dynamicSize) {
                this.restoreRealScroll();
            }
            if (widthChange && this.popupSupport && this.popupDiv) {
                let popupParent = getParent(this.popupDiv);
                popupParent.style.width = (this.getVisibleColsDim() - this.colGroupDimension) + 'px';
                this.requestFrame(() => {
                    if (this.popupDiv)
                        this.renderPopup(this.popupDiv); // re-render popup on width change. Used in next frame, so size is properly adjusted before it is rendered
                }, 'renderPopup');
            }
            if (this.dynamicSize && this.scrollingParent && scrollChanged) {
                this.parentScrollFrame(); // update header position when something's changed
            }
            else if (redraw)
                this.deferredDraw();
        }
    }
    handle_layoutchange(e) {
        this.recalcLayout(true);
    }
    handleCanvasScroll(e) {
        // handle scrolling even for dynamicSize, it could be horizontal scrolling in grid
        this.scrollUpdateNeeded = true;
        this.deferredDraw();
    }
    /**
    Should clean up all the control stuff, i.e. mainly unlisten events.

    @method cleanUp
    */
    cleanUp() {
        this._openingPopupTimer = -1;
        app.unlisten(this.header); // unregisters all on this.header    
        app.unlisten(this.viewport); // unregisters all on this.viewport
        if (this._settingDSPromise)
            cancelPromise(this._settingDSPromise);
        // Clean up all items/divs and group headers    
        this.clearDivs();
        // Clean all pop-ups
        // eslint-disable-next-line no-cond-assign
        for (let popup; popup = this.popupCache.pop();)
            removeElement(popup.div);
        if (this.unlisteners) {
            forEach(this.unlisteners, function (unlistenFunc) {
                unlistenFunc();
            });
            this.unlisteners = undefined;
        }
        app.unlisten(this.canvas);
        if (this._dataSource)
            this.dataSource = null; // remove datasource with events, last, so previous unlisten functions can access datasource        
        super.cleanUp();
    }
    updateDropEffect(itemIndex) {
        let dropAfter = -1;
        let dropBefore = -1;
        if (itemIndex >= 0) {
            if (itemIndex > 0)
                dropAfter = itemIndex - 1;
            if (itemIndex < this.itemCount)
                dropBefore = itemIndex;
        }
        else {
            dropBefore = itemIndex;
        }
        let divBefore;
        if (dropBefore >= 0)
            divBefore = this.getDiv(dropBefore);
        let divAfter;
        if (dropAfter >= 0)
            divAfter = this.getDiv(dropAfter);
        if (this.oldDropBefore) {
            if (this.oldDropBefore != divBefore) {
                this.setUpTransition(this.oldDropBefore, 'data-dropeffect');
                this.oldDropBefore.removeAttribute('data-dropbefore');
                this.oldDropBefore = null;
            }
        }
        if (this.oldDropAfter) {
            if (this.oldDropAfter != divAfter) {
                this.setUpTransition(this.oldDropAfter, 'data-dropeffect');
                this.oldDropAfter.removeAttribute('data-dropafter');
                this.oldDropAfter = undefined;
            }
        }
        if (this.dragging) {
            if (divAfter && this.oldDropAfter != divAfter) {
                divAfter.setAttribute('data-dropeffect', 1);
                divAfter.setAttribute('data-dropafter', 1);
                this.oldDropAfter = divAfter;
            }
            if (divBefore && this.oldDropBefore != divBefore) {
                divBefore.setAttribute('data-dropeffect', 1);
                divBefore.setAttribute('data-dropbefore', 1);
                this.oldDropBefore = divBefore;
            }
        }
    }
    adjustScroll(value) {
        if (typeof this.smoothScrollOrigin != 'undefined') {
            this.smoothScrollAdjust += value;
        }
        else {
            this.setScrollOffset(this.getScrollOffset() + value);
        }
    }
    getScrollOffset() {
        if (this.dynamicSize && this.scrollingParent) {
            return this._parentScrollTop - this._containerOffsetTop;
        }
        else {
            if (this.scrollUpdateNeeded) {
                this.canvasScrollLeft = this.canvas.scrollLeft;
                this.canvasScrollTop = this.canvas.scrollTop;
            }
            if (this.isHorizontal)
                return this.canvasScrollLeft;
            else {
                return this.canvasScrollTop;
            }
        }
    }
    getSmoothScrollOffset() {
        let scrollTop = this.getScrollOffset();
        if (this.dynamicSize) {
            return scrollTop;
        }
        else {
            if (typeof this.smoothScrollOrigin != 'undefined') {
                scrollTop = this.smoothScrollTarget;
                let newTime = window.performance.now();
                let adjust = this.smoothScrollAdjust;
                this.smoothScrollAdjust = 0;
                let res;
                if (newTime - this.smoothScrollTime >= this.smoothScrollTimeLimit) {
                    this.smoothScrollOrigin = undefined;
                    res = scrollTop + adjust;
                    this.setScrollOffset(res); // To update the scrollbar position in case we scrolled beyond original height of the viewport
                }
                else {
                    this.smoothScrollOrigin += adjust;
                    this.smoothScrollTarget += adjust;
                    res = Math.max(0, this.smoothScrollOrigin + (this.smoothScrollTarget - this.smoothScrollOrigin) * Math.pow((newTime - this.smoothScrollTime) / this.smoothScrollTimeLimit, 0.6));
                }
                return res;
            }
            else
                return scrollTop;
        }
    }
    setSmoothScrollOffset(newValue, canScrollBeyond /*To allow scrolling lower than is the current viewport height*/) {
        if (this.dynamicSize && this.scrollingParent) {
            if (this.scrollingParent.controlClass && this.scrollingParent.controlClass.setSmoothScrollOffset)
                this.scrollingParent.controlClass.setSmoothScrollOffset(newValue + this.container.offsetTop, canScrollBeyond);
            else
                this.setScrollOffset(newValue, canScrollBeyond);
        }
        else {
            let origin = this.getSmoothScrollOffset();
            this.setScrollOffset(newValue, canScrollBeyond);
            if (this.smoothScroll) {
                this.smoothScrollTarget = (canScrollBeyond ? newValue : this.getScrollOffset());
                this.smoothScrollOrigin = origin;
                this.smoothScrollTime = window.performance.now();
            }
        }
    }
    // scroll parent, so this LV is as visible as possible, possible leaving space for external heading
    scrollParentToBestView(headingHeight) {
        headingHeight = headingHeight || 0;
        let scTop = undefined;
        if (this._parentScrollTop > (this._containerOffsetTop - headingHeight)) {
            scTop = -headingHeight;
        }
        else if ((this._containerOffsetHeight + this._containerOffsetTop) > (this._parentScrollTop + this._parentOffsetHeight)) {
            scTop = Math.min(-headingHeight, this._containerOffsetHeight - this._parentOffsetHeight);
        }
        if (scTop !== undefined)
            this.setSmoothScrollOffset(scTop);
    }
    setScrollOffset(newValue, canScrollBeyond) {
        if (this.dynamicSize && this.scrollingParent) {
            if (this.scrollingParent.controlClass && this.scrollingParent.controlClass.setScrollOffset)
                this.scrollingParent.controlClass.setScrollOffset(newValue + this.container.offsetTop);
            else
                this.scrollingParent.scrollTop = newValue + this.container.offsetTop;
            this.parentScrollFrame();
        }
        else {
            this.smoothScrollOrigin = undefined;
            if (this.isHorizontal) {
                this.canvas.scrollLeft = newValue;
                this.canvasScrollLeft = (canScrollBeyond ? newValue : this.canvas.scrollLeft);
            }
            else {
                this.canvas.scrollTop = newValue;
                this.canvasScrollTop = (canScrollBeyond ? newValue : this.canvas.scrollTop);
            }
        }
    }
    resetScrollbars() {
        this.canvas.scrollLeft = 0;
        this.canvasScrollLeft = this.canvas.scrollLeft;
        this.canvas.scrollTop = 0;
        this.canvasScrollTop = this.canvas.scrollTop;
    }
    getVisibleRowsDimVirtual() {
        if (this.dynamicSize) {
            return this._parentOffsetHeight - this._headerOffsetHeight;
        }
        else {
            if (this.isHorizontal)
                return this.getVisibleRect().width;
            else
                return this.getVisibleRect().height;
        }
    }
    getVisibleRowsDim() {
        if (this.isHorizontal)
            return this.getVisibleRect().width;
        else
            return this.getVisibleRect().height;
    }
    getVisibleColsDim() {
        if (this.isHorizontal)
            return this.canvasHeight;
        else
            return this.canvasWidth;
    }
    getItemForCanvas(row, col) {
        if (this.isHorizontal)
            return this.getItemFromAbsolutePosition(row, col, true /*include approximate results*/);
        else
            return this.getItemFromAbsolutePosition(col, row, true);
    }
    getItemFromRelativePosition(x, y, approxResults) {
        if (!this.dynamicSize)
            if (this.isHorizontal)
                x += this.getSmoothScrollOffset();
            else
                y += this.getSmoothScrollOffset();
        return this.getItemFromAbsolutePosition(x, y, approxResults);
    }
    getItemFromAbsolutePosition(x, y, approxResults) {
        if (approxResults === undefined)
            approxResults = false;
        let row;
        let col;
        if (this.isHorizontal) {
            row = x;
            col = y;
        }
        else {
            row = y;
            col = x;
        }
        if (!this.isGrid && this.isGrouped && this.ignoreMouseOnGroup)
            col -= Math.max(0, this.colGroupDimension - this.canvasScrollLeft);
        else
            col -= this.colGroupDimension;
        if ((row < 0 || col < 0) && !approxResults)
            return;
        if (col < 0) // For approximate results we accept negative values (useful for grouping)
            col = 0;
        let itemIndex;
        let origrow = row;
        // Adjust for skipped regions
        for (let i = this.skips.length - 1; i >= 0; i--) { // we need to go backward when we have more than one skips
            let skip = this.skips[i];
            if (skip._startPx <= row) {
                if (row < skip._startPx + skip.reservePx && !approxResults)
                    return undefined; // We are inside the reserved region
                row -= skip.reservePx;
            }
        }
        if (this.isGrouped) {
            let group = this.getOffsetGroup(row);
            if (!group) // E.g. groups not provided yet
                return undefined;
            if (this.isGrid) {
                itemIndex = group.index + Math.min(Math.floor((row - group.offset) / (this.rowDimension + this.itemRowSpacing)) * this.itemsPerRow +
                    Math.floor(col / (this.colDimension + this.itemHorzSpacing + this.itemRedistSpacing)), group.visibleTracks - 1);
            }
            else {
                itemIndex = group.index + Math.min(Math.floor((row - group.offset) / (this.rowDimension + this.itemRowSpacing)), group.visibleTracks - 1);
            }
        }
        else {
            if (this.isGrid) {
                itemIndex = Math.min(Math.floor(row / (this.rowDimension + this.itemRowSpacing)) * this.itemsPerRow +
                    Math.floor(col / (this.colDimension + this.itemHorzSpacing + this.itemRedistSpacing)), this.itemCount - 1);
            }
            else {
                itemIndex = Math.min(Math.floor(row / (this.rowDimension + this.itemRowSpacing)), this.itemCount - 1);
            }
        }
        if (itemIndex < 0)
            return undefined;
        if (!approxResults) {
            // Make sure that the calculated item rectangle contains the point    
            let rect = this.getItemRect(itemIndex);
            col += this.colGroupDimension;
            if (origrow < rect.top || origrow >= rect.top + rect.height || col < rect.left || col >= rect.left + rect.width)
                return undefined;
        }
        return itemIndex;
    }
    addSkipsToRow(row) {
        for (let i = 0; i < this.skips.length; i++) {
            let skip = this.skips[i];
            if (row >= skip._startPx)
                row += skip.reservePx;
        }
        return row;
    }
    getItemTopOffset(itemIndex) {
        let res;
        if (this.isGrouped) {
            let group = this.getItemGroup(itemIndex);
            if (!group)
                return 0;
            res = Math.floor((itemIndex - group.index) / this.itemsPerRow) * (this.rowDimension + this.itemRowSpacing) +
                group.offset;
        }
        else
            res = Math.floor(itemIndex / this.itemsPerRow) * (this.rowDimension + this.itemRowSpacing);
        return this.addSkipsToRow(res);
    }
    getItemLeft(itemIndex) {
        if (this.isGrouped) {
            let group = this.getItemGroup(itemIndex);
            if (group) {
                let items = (itemIndex - group.index);
                return this.colGroupDimension + (items - Math.floor(items / this.itemsPerRow) * this.itemsPerRow) * (this.colDimension + this.itemRedistSpacing);
            }
        }
        return (itemIndex - Math.floor(itemIndex / this.itemsPerRow) * this.itemsPerRow) * (this.colDimension + this.itemRedistSpacing);
    }
    getItemRect(itemIndex) {
        return {
            top: this.getItemTopOffset(itemIndex),
            left: this.getItemLeft(itemIndex),
            width: this.colDimension,
            height: this.rowDimension
        };
    }
    getItemTopRelativeOffset(itemIndex) {
        return this.getItemTopOffset(itemIndex) - this.getScrollOffset();
    }
    getScrollBottom() {
        return this.getScrollOffset() - this._headerOffsetHeight + Math.max(this.getVisibleRowsDim(), this._parentOffsetHeight);
    }
    scrollToView(top, bottom, aboveShift) {
        if (fullLVDebug)
            ODS('ScrollToView: ' + top + ', current: ' + this.getScrollOffset());
        let availableH = Math.max(this.getVisibleRowsDim(), this._parentOffsetHeight);
        let scrollOffset = this.getScrollOffset();
        let itemH = bottom - top;
        if ((top < scrollOffset) || (itemH > availableH)) {
            if ((top > scrollOffset) || (itemH <= availableH)) // scroll only if we already do not show the content all over the available area to avoid unintended scroll, #15803
                this.setSmoothScrollOffset(top + (aboveShift || 0));
        }
        else {
            let scrollBottom = scrollOffset - this._headerOffsetHeight;
            scrollBottom += availableH; // #15803
            if (this.dynamicSize && this.scrollingParent && (this.scrollingParent.scrollWidth > this.scrollingParent.clientWidth))
                scrollBottom -= getScrollbarWidth(); // LS: so that item is fully visible even when there is bottom scrollbar in scrolling parent (#15185 - item 14)
            if (bottom > scrollBottom)
                this.setSmoothScrollOffset(scrollOffset + bottom - scrollBottom + (aboveShift || 0), true /*can scroll beyond current height*/);
        }
    }
    setItemFullyVisible(itemIndex, immediately) {
        this._itemToShow = itemIndex;
        if (immediately) {
            this._setItemFullyVisible();
            this.invalidateScrollPos = true;
            this.saveRealScroll();
        }
        else
            this.deferredDraw();
    }
    _setItemFullyVisible() {
        let itemIndex = this._itemToShow;
        if (itemIndex !== undefined) {
            this._itemToShow = undefined;
            let offset = this.getItemTopOffset(itemIndex);
            this.scrollToView(offset, offset + this.rowDimension);
        }
    }
    setItemFullyVisibleCentered(itemIndex) {
        let offset = this.getItemTopOffset(itemIndex);
        this.setSmoothScrollOffset(offset + (this.rowDimension - this.getVisibleRowsDim()) / 2);
    }
    setFocusedFullyVisible() {
        this.setItemFullyVisible(this.focusedIndex || 0);
    }
    isItemFullyVisible(itemIndex) {
        let offset = this.getItemTopRelativeOffset(itemIndex);
        return (offset >= 0) && (offset + this.rowDimension < this.getVisibleRowsDim());
    }
    setfocusedIndexAndDeselectOld(itemIndex) {
        if (this.focusedAlsoSelected)
            return this.setFocusedAndSelectedIndex(itemIndex);
        else {
            this.focusedIndex = itemIndex;
            return dummyPromise();
        }
    }
    handleFocusChanged(newIndex, oldIndex) {
        if (newIndex == oldIndex)
            return; // #15426
        if (this.ignoreShiftFocusChange) {
            this.ignoreShiftFocusChange = false;
        }
        else {
            this._shiftFocusedItem = newIndex;
            this._groupShiftFocusedID = undefined;
        }
        let div;
        if (oldIndex >= 0) {
            div = this.getDiv(oldIndex);
            if (div) {
                div.forceRebind = true;
            }
        }
        if (newIndex >= 0) {
            div = this.getDiv(newIndex);
            if (div)
                div.forceRebind = true;
        }
        this.deferredDraw();
        this.onFocusChanged(newIndex);
    }
    handleSortChanged(_itemObjectToShow) {
        if (_itemObjectToShow) {
            if (this.dataSource) {
                let item = _itemObjectToShow;
                this.dataSource.locked(() => {
                    let idx = this.dataSource.indexOf(item);
                    if (idx >= 0) {
                        this._itemToShow = idx;
                        this.focusedIndex = idx;
                    }
                });
            }
        }
        this.deferredDraw();
    }
    redrawFocusedItem(newState) {
        let oldState = this.focusVisible;
        this.focusVisible = newState;
        if (oldState !== newState) {
            let div = this.getDiv(this.focusedIndex);
            if (div)
                this.handleBinding(div, this.focusedIndex); // refresh
        }
        this.focusRefresh(newState);
    }
    setSelectedIndex(itemIndex, dontClearSelection) {
        let ds = this._dataSource;
        if (ds && itemIndex < ds.count && itemIndex >= 0) {
            return ds.modifyAsync(() => {
                if (itemIndex < ds.count && (itemIndex >= 0) && !ds.isSelected(itemIndex)) {
                    if (!dontClearSelection)
                        ds.clearSelection();
                    ds.setSelected(itemIndex, true);
                    this.raiseItemSelectChange(itemIndex);
                }
            }, { onlyFlags: true });
        }
        return dummyPromise();
    }
    setFocusedAndSelectedIndex(itemIndex) {
        this._requestedFocAndSelectIdx = itemIndex;
        if (itemIndex != this.focusedIndex) {
            let ds = this._dataSource;
            if (ds) {
                return ds.modifyAsync(() => {
                    if /* still */ (this._requestedFocAndSelectIdx == itemIndex) {
                        ds.clearSelection();
                        if ((itemIndex < ds.count) && (itemIndex >= 0)) {
                            ds.setSelected(itemIndex, true);
                            this.raiseItemSelectChange(itemIndex);
                        }
                        else
                            ds.clearSelection();
                        this.focusedIndex = itemIndex; // LS: needs to be set after the selection - some components listen for 'focuschange' event and creates context menu based on selected items (#15083)                    
                    }
                }, { onlyFlags: true });
            }
        }
        return dummyPromise();
    }
    getItemColumn(itemIndex) {
        if (this.isGrouped) {
            let group = this.getItemGroup(itemIndex);
            return (itemIndex - group.index) % this.itemsPerRow;
        }
        else
            return itemIndex % this.itemsPerRow;
    }
    getItemAtColumnOrLess(itemIndex, column) {
        if (this.isGrouped) {
            let group = this.getItemGroup(itemIndex);
            let offset = (itemIndex - group.index);
            return Math.min(group.index + group.itemCount - 1, itemIndex +
                Math.max(column - offset % this.itemsPerRow, 0));
        }
        else
            return Math.min(this.itemCount - 1, itemIndex + column - itemIndex % this.itemsPerRow);
    }
    getItemRowDown(itemIndex) {
        let item = itemIndex + this.itemsPerRow;
        let next = (item < this.itemCount ? item : itemIndex);
        if ((this.showRowCount > 0) && (this.itemsPerRow > 0)) {
            let currRow = Math.floor(next / this.itemsPerRow);
            if (currRow >= this.showRowCount)
                next = itemIndex;
        }
        if (this.isGrouped) {
            let group = this.getItemGroup(itemIndex);
            let group2 = this.getItemGroup(next);
            if (group.index != group2.index) {
                group2 = this.getNextGroup(group);
                next = group2.index + Math.min(group2.itemCount - 1, this.getItemColumn(itemIndex));
            }
        }
        return next;
    }
    getItemRowUp(itemIndex) {
        let item = itemIndex - this.itemsPerRow;
        let next = (item >= 0 ? item : itemIndex);
        if (this.isGrouped) {
            let group = this.getItemGroup(itemIndex);
            let group2 = this.getItemGroup(next);
            if (group && group2 && (group.index != group2.index)) {
                group2 = this.getPrevGroup(group);
                next = group2.index + Math.min(group2.itemCount - 1, Math.floor((group2.itemCount - 1) / this.itemsPerRow) * this.itemsPerRow + this.getItemColumn(itemIndex));
            }
        }
        return next;
    }
    ignoreHotkey(hotkey) {
        let ar = [];
        if (this.focusedIndex >= 0) {
            ar = ['Right', 'Left', 'Up', 'Down', 'Enter', 'PageUp', 'PageDown'];
            if (this.checkboxes)
                ar.push('Space');
            if (window.uitools.getCanEdit())
                ar.push('F2');
        }
        if (this.enableIncrementalSearch && this._searchBuffer)
            ar.push('Space');
        return inArray(hotkey, ar, true /* ignore case */);
    }
    handle_keyup(e) {
        if (this.disabled)
            return;
    }
    handle_keydown(e) {
        if (this.disabled)
            return;
        let newFocus = this.focusedIndex;
        let lv = this;
        function handleDown() {
            if (lv.focusedIndex < 0)
                newFocus = 0;
            else
                newFocus = lv.getItemRowDown(lv.focusedIndex);
        }
        function handleRight() {
            if (lv.focusedIndex < 0)
                newFocus = 0;
            else
                newFocus = Math.min(lv.focusedIndex + 1, lv.itemCount - 1);
            if ((lv.showRowCount > 0) && (lv.itemsPerRow > 0)) {
                let currRow = Math.floor(newFocus / lv.itemsPerRow);
                if (currRow >= lv.showRowCount)
                    newFocus = lv.focusedIndex;
            }
        }
        function handleLeft() {
            if (lv.focusedIndex < 0)
                newFocus = 0;
            else
                newFocus = Math.max(lv.focusedIndex - 1, 0);
        }
        function handleUp() {
            if (lv.focusedIndex < 0)
                newFocus = 0;
            else
                newFocus = lv.getItemRowUp(lv.focusedIndex);
        }
        function handlePageDown() {
            let item = lv.focusedIndex;
            let column = lv.getItemColumn(item);
            let itemOffset = lv.getItemTopOffset(lv.focusedIndex);
            while (true) {
                let nextItem = lv.getItemRowDown(item);
                if (nextItem == item || !lv.isItemFullyVisible(nextItem))
                    break;
                item = nextItem;
            }
            if (item != lv.focusedIndex) { // Focus can be moved a bit down without scrolling
                newFocus = item;
                if (itemOffset != lv.getItemTopOffset(item))
                    newFocus = lv.getItemAtColumnOrLess(newFocus, column);
            }
            else { // Scrolling is needed
                while (true) {
                    let nextItem = lv.getItemRowDown(item);
                    let nextOffset = lv.getItemTopOffset(nextItem);
                    if (nextItem == item || nextOffset - itemOffset >= lv.getVisibleRowsDimVirtual())
                        break;
                    item = nextItem;
                }
                newFocus = lv.getItemAtColumnOrLess(item, column);
            }
        }
        function handlePageUp() {
            let item = lv.focusedIndex;
            let column = lv.getItemColumn(item);
            let itemOffset = lv.getItemTopOffset(lv.focusedIndex);
            while (true) {
                let nextItem = lv.getItemRowUp(item);
                if (nextItem == item || !lv.isItemFullyVisible(nextItem))
                    break;
                item = nextItem;
            }
            if (item != lv.focusedIndex) { // Focus can be moved a bit up without scrolling
                newFocus = item;
                if (itemOffset != lv.getItemTopOffset(item))
                    newFocus = lv.getItemAtColumnOrLess(newFocus, column);
            }
            else { // Scrolling is needed
                while (true) {
                    let nextItem = lv.getItemRowUp(item);
                    let nextOffset = lv.getItemTopOffset(nextItem);
                    if (nextItem == item || itemOffset - nextOffset >= lv.getVisibleRowsDimVirtual())
                        break;
                    item = nextItem;
                }
                newFocus = lv.getItemAtColumnOrLess(item, column);
            }
        }
        let handled = false;
        switch (friendlyKeyName(e)) {
            case 'Enter':
                {
                    let div = this.getDiv(this.focusedIndex);
                    if (div && !e.ctrlKey && !e.altKey && !e.shiftKey) // so that Ctrl+Enter is not taken like Enter
                     {
                        let item = this.getItem(div.itemIndex);
                        if (item)
                            this.raiseEvent('itementer', {
                                item: item,
                                div: div
                            });
                        handled = true;
                    }
                }
                break;
            case 'Esc':
                if (!e.ctrlKey && !e.altKey && !e.shiftKey) {
                    if (this.isPopupShown()) {
                        this.closePopup();
                        handled = true;
                    }
                    this.selectionMode = false;
                    handled = true;
                }
                break;
            case 'Down':
                if (!e.altKey) {
                    if (e.ctrlKey && this._lastSearchBuffer)
                        this.performIncrementalSearch(this._lastSearchBuffer, false /* reverse order */, true /* next occurence */);
                    else if (this.isHorizontal)
                        handleRight();
                    else
                        handleDown();
                    handled = (newFocus != this.focusedIndex);
                }
                break;
            case 'Right':
                if (!e.altKey) {
                    if (this.isHorizontal)
                        handleDown();
                    else
                        handleRight();
                    handled = (newFocus != this.focusedIndex);
                }
                break;
            case 'Left':
                if (!e.altKey) {
                    if (this.isHorizontal)
                        handleUp();
                    else
                        handleLeft();
                    handled = (newFocus != this.focusedIndex);
                }
                break;
            case 'Up':
                if (!e.altKey) {
                    if (e.ctrlKey && this._lastSearchBuffer)
                        this.performIncrementalSearch(this._lastSearchBuffer, true /* reverse order */, true /* next occurence  */);
                    else if (this.isHorizontal)
                        handleLeft();
                    else
                        handleUp();
                    handled = (newFocus != this.focusedIndex);
                }
                break;
            case 'Home':
                if (!this.dynamicSize || (e.shiftKey && this.multiselect /* #16955 */)) {
                    if (!e.altKey) {
                        if (this.itemCount > 0)
                            newFocus = 0;
                        handled = true;
                    }
                }
                break;
            case 'End':
                if (!this.dynamicSize || (e.shiftKey && this.multiselect /* #16955 */)) {
                    if (!e.altKey) {
                        if (this.itemCount > 0)
                            newFocus = this.itemCount - 1;
                        handled = true;
                    }
                }
                break;
            case 'PageDown':
                if (!e.altKey) {
                    if (this.focusedIndex < 0)
                        newFocus = 0;
                    else
                        handlePageDown();
                    handled = true;
                }
                break;
            case 'PageUp':
                if (!e.altKey) {
                    if (this.focusedIndex < 0)
                        newFocus = 0;
                    else
                        handlePageUp();
                    handled = true;
                }
                break;
            case 'Space':
                if (e.ctrlKey && this.multiselect) {
                    if (this.focusedIndex >= 0) {
                        this.focusedShiftItem = this.focusedIndex;
                        let ds = this._dataSource;
                        ds.modifyAsync(() => {
                            if ((this.focusedIndex < ds.count) && (this.focusedIndex >= 0)) {
                                let _select = !ds.isSelected(this.focusedIndex);
                                ds.setSelected(this.focusedIndex, _select);
                                if (_select)
                                    this.raiseItemSelectChange(this.focusedIndex);
                            }
                        }, { onlyFlags: true });
                        handled = true;
                    }
                }
                else {
                    if (this.checkboxes) {
                        this.invertCheckStateForSelected();
                        handled = true;
                    }
                }
                break;
            case 'F2':
                if (window.uitools.getCanEdit()) {
                    this.editStart();
                    handled = true;
                }
                break;
            case '+': // '+'
                if (e.ctrlKey) {
                    this.zoomIn();
                    handled = true;
                }
                break;
            case '-': // '-'
                if (e.ctrlKey) {
                    this.zoomOut();
                    handled = true;
                }
                break;
            case 'a': // 'a'
                if (this.multiselect && e.ctrlKey && !e.altKey && !e.shiftKey) {
                    let ds = this.dataSource;
                    if (ds && ds.selectRangeAsync)
                        ds.selectRangeAsync(0, ds.count - 1);
                    handled = true;
                }
                break;
            default:
                handled = false;
        }
        if (this.enableIncrementalSearch && !handled && !e.ctrlKey && !e.altKey && !e.metaKey && e.key && (e.key.length === 1)) {
            let ignore = false;
            if (e.shiftKey) { // shift is needed for capitals (#15106 / 11)                    
                if (window.hotkeys && window.hotkeys.getHotkeyData('Shift+' + window.friendlyKeyName(e)))
                    ignore = true; // #18628: Shift+Character Hotkey also executes as character
            }
            if (!ignore)
                this._handleIncrementalSearch(e.key);
            handled = true; // always handled, so it will not jump to filter section in case focus is not changed
        }
        if (handled) {
            e.stopPropagation();
            e.preventDefault(); // Needed at least for dynamicSize LVs in order to prevent scrolling of the parent element on arrows
            this._useMouseHover = false;
            this.updateHover();
        }
        if (handled && e.keyCode > 18) // any key pressed (not just shift or so)
            this.focusVisible = true; // After a keyboard operation, make focus rectangle visible
        if (newFocus != this.focusedIndex) {
            let oldShiftItem = this._shiftFocusedItem;
            let oldShiftGroupID = this._groupShiftFocusedID;
            if (e.shiftKey && this.multiselect) {
                if (lv.selectingRange) // not finished previous selection, do not call yet, it would cause #18351
                    return;
                this.focusedIndex = newFocus;
                this._shiftFocusedItem = oldShiftItem;
                this._groupShiftFocusedID = oldShiftGroupID;
                lv.selectingRange = true;
                this._dataSource.selectRangeAsync(this.focusedIndex, this.getShiftFocusedIndex(), this.isShiftSelect(), !e.ctrlKey /* clear selection */).then1(function () {
                    lv.selectingRange = false;
                });
                this.closePopup();
                if (this.automaticSelectionMode)
                    this.selectionMode = true;
            }
            else if (e.ctrlKey && this.multiselect) {
                this.focusedIndex = newFocus;
                this._shiftFocusedItem = oldShiftItem;
                this._groupShiftFocusedID = oldShiftGroupID;
                this.closePopup();
                if (this.automaticSelectionMode)
                    this.selectionMode = true;
            }
            else {
                this.setfocusedIndexAndDeselectOld(newFocus).then1(() => {
                    if (this.isPopupShown())
                        this.showPopup(newFocus);
                    this.setFocusedFullyVisible();
                    if (this.isGrid)
                        this.container.focus(); // LS: this is workaround for #19611, I haven't figured out why focus is lost sometimes
                });
            }
            this.setFocusedFullyVisible(); // #17009 / #17568
        }
        this.focusRefresh(this.focusVisible);
        this.afterUserInteraction();
    }
    showToast(message) {
        let scrollLeft;
        if (this.dynamicSize && this.scrollingParent)
            scrollLeft = this.scrollingParent.scrollLeft;
        else
            scrollLeft = this.canvas.scrollLeft;
        let rect = this.container.getBoundingClientRect();
        let visRect = this.getVisibleRect();
        let _left = rect.left + scrollLeft;
        let _right = _left + visRect.width;
        uitools.toastMessage.show(message, {
            disableClose: true,
            delay: 3000,
            left: _left,
            right: _right
        });
    }
    _handleIncrementalSearch(letter, reverseOrder, nextOccurence) {
        if (letter) {
            if (this._searchBuffer) {
                this._searchBuffer = this._searchBuffer + letter;
            }
            else {
                if (letter == ' ')
                    return; // skip the first space key (when there is nothing in the _searchBuffer yet)
                if (window.hotkeys && window.hotkeys.getHotkeyData(letter))
                    return; // #19475: Contextual search shouldn't override hotkeys    
                this._searchBuffer = letter;
            }
        }
        if (!this.parentView && !this.supressIncrementalSearchToasts) // supress toast messages when we are placed into a view, search bar is taking it
            this.showToast(_('Scroll to') + ': "' + this._searchBuffer + '" (' + sprintf(_('Use %s for the next match'), '"Ctrl+Down"') + ') ' + this._incrementalSearchMessageSuffix(this._searchBuffer));
        if (!this.performIncrementalSearch(this._searchBuffer, reverseOrder, nextOccurence)) {
            if (nextOccurence && !this.parentView && !this.supressIncrementalSearchToasts && this._searchBuffer)
                this.showToast('"' + this._searchBuffer + '" ' + _('phrase not found') + this._incrementalSearchMessageSuffix(this._searchBuffer));
        }
        this.raiseEvent('incrementalsearch', {
            controlClass: this,
            phrase: this._searchBuffer,
            reverseOrder: reverseOrder
        }, true, true);
    }
    performIncrementalSearch(searchPhrase, reverseOrder, nextOccurence) {
        if (!searchPhrase || searchPhrase == '')
            return;
        this._searchBuffer = searchPhrase;
        this._lastSearchBuffer = this._searchBuffer;
        this.requestTimeout(() => {
            this._searchBuffer = undefined;
        }, 1000 /* ms (#15185 - item 10) */, 'incSearchClearBufferTimeout');
        let _success = true;
        let oldIndex = this.focusedIndex;
        let newIndex = this.incrementalSearch(this._searchBuffer, reverseOrder, nextOccurence);
        if (newIndex >= 0) {
            this.setfocusedIndexAndDeselectOld(newIndex).then(() => {
                this.setFocusedFullyVisible(); // #17045
            });
        }
        if (oldIndex == newIndex && nextOccurence) {
            _success = false;
        }
        else if (newIndex < 0) {
            _success = false; // no occurence
        }
        else {
            if (oldIndex >= 0)
                if (((newIndex < oldIndex) && !reverseOrder) || ((newIndex > oldIndex) && reverseOrder))
                    _success = false;
        }
        return _success;
    }
    _incrementalSearchMessageSuffix(phrase) {
        return ''; // is overriden by descendants (e.g. TracklistView)
    }
    incrementalSearch(searchPhrase, reverseOrder, nextOccurence) {
        let result = this.focusedIndex;
        let ds = this.dataSource;
        if (ds && ds.getIndexByPrefix) {
            let startIndex = 0;
            if (this.focusedIndex >= 0) {
                if (reverseOrder) {
                    if (nextOccurence)
                        startIndex = this.focusedIndex;
                    else
                        startIndex = this.focusedIndex + 1;
                }
                else {
                    reverseOrder = false;
                    if (nextOccurence)
                        startIndex = this.focusedIndex + 1;
                    else
                        startIndex = this.focusedIndex;
                }
            }
            result = ds.getIndexByPrefix(searchPhrase, startIndex, reverseOrder);
        }
        return result;
    }
    /**
    Starts inline editing of the focused item.

    @method editStart
    */
    editStart() { }
    /**
    Confirms the current inline edit.

    @method editSave
    */
    editSave(continueEdit /* this value will be true when saved valued using tab or keydown */, newItemSelected /* new item was selected by mouse */) {
        this.inEdit = undefined;
    }
    /**
    Cancels the current inline edit.

    @method editCancel
    */
    editCancel() {
        this.inEdit = undefined;
    }
    handleItemLongTouch(div, e) {
        if (this.disabled)
            return;
        let item = this.getItem(div.itemIndex);
        this.focusedIndex = -1;
        this.setFocusedAndSelectedIndex(div.itemIndex);
        this.raiseEvent('touchlongclick', {
            item: item,
            div: div
        }, true, true, div);
    }
    getShiftFocusedIndex() {
        if (this._groupShiftFocusedID !== undefined) {
            let group = this._dataSource.getGroupByID(this._groupShiftFocusedID);
            if (!group)
                return 0;
            if (group.index < this.focusedIndex)
                return group.index;
            else
                return group.index + group.itemCount - 1;
        }
        else
            return this._shiftFocusedItem;
    }
    // Returns whether the current operation should select or unselect
    isShiftSelect() {
        let focus = this.getShiftFocusedIndex();
        if (focus < 0 || focus >= this._dataSource.count || this.selectionMode)
            return true;
        else {
            let ret = false;
            this.dataSource.locked(function () {
                ret = this._dataSource.isSelected(focus);
            }.bind(this));
            return ret;
        }
    }
    afterUserInteraction() { }
    handleItemMouseDown(div, e) {
        e.stopPropagation();
        if (this.disabled)
            return;
        let ds = this._dataSource;
        if (!ds)
            return;
        // check selection or drag
        let canDrag = this.handleLassoStart(div, e);
        this.makeDraggable(div, canDrag && !!this.dndEventsRegistered);
        let wasUsingTouch = usingTouch;
        // @ts-ignore
        let doMiddleClick = (e.which === 2 && typeof this.handleItemMiddleClick === 'function'); // #19042
        let pr = ds.modifyAsync(() => {
            let index = div.itemIndex;
            if ((index < ds.count) && (index >= 0)) {
                if (e.shiftKey && this.multiselect) {
                    this.ignoreShiftFocusChange = true;
                    this.focusedIndex = index;
                    ds.selectRangeAsync(this.focusedIndex, this.getShiftFocusedIndex(), this.isShiftSelect(), !e.ctrlKey);
                }
                else if (doMiddleClick) {
                    // #16960: Add optional middle wheel click handler for classes that extend ListView
                    // 	handleItemMiddleClick acts as an override for all 
                    // @ts-ignore
                    this.handleItemMiddleClick(div, e);
                }
                else {
                    if (this.focusedIndex == index) {
                        let ignore = ((this.lastMouseDiv === div) && (div.lastMouseUp) && (Date.now() - div.lastMouseUp < 3000)); // to not interfere with title editing (#15927 - item 2b)
                        if (!ignore)
                            this.onFocusChanged(index); // to emit 'focuschange' event even when the same node is clicked again, needed for media tree (#12717 - item 3) and playlist tree (#15926 - 7b)
                    }
                    else
                        this.focusedIndex = index;
                    if ((e.ctrlKey && this.multiselect) || (this.multiselect && wasUsingTouch && this.selectionMode)) {
                        let _select = !ds.isSelected(index);
                        ds.setSelected(index, _select);
                        if (_select)
                            this.raiseItemSelectChange(index);
                        else
                            div.removeAttribute('data-hover');
                    }
                    else {
                        if (!ds.isSelected(index)) {
                            ds.clearSelection();
                            ds.setSelected(index, true);
                        }
                        this.raiseItemSelectChange(index);
                    }
                }
            }
            this._lastFocusChangingPromise = undefined;
        }, { onlyFlags: true });
        if (e.button === 2) // right button
            this._contextMenuPromises.push(pr); // to wait for 'focuschange' in the 'contextmenu' handler
        if (doMiddleClick) {
            e.preventDefault(); // #19042 - preventDefault() must be done immediately, not in a callback
        }
    }
    canUseLasso(e) {
        if (e.target.nodeName !== 'LABEL') {
            // lasso is enabled only in 'non-content' part of the list (out of text)
            let content = e.target.innerText;
            if (content) {
                let line = content;
                let nl = line.indexOf('\n');
                if (nl > 0)
                    line = line.substr(0, nl + 1);
                let w = getTextWidth(line, e.target);
                return e.offsetX > w;
            }
        }
        return false;
    }
    handleLassoStart(div, e) {
        if (this.selectionMode)
            return;
        let ret = true;
        let isLeftButton = (e.button === 0);
        let isSelected = false;
        if (this.dataSource && div && (div.itemIndex >= 0)) {
            this.dataSource.locked(() => {
                isSelected = this.dataSource.isSelected(div.itemIndex);
            });
        }
        this._lassoSelectionStart = undefined;
        if (this.multiselect && isLeftButton && !e.shiftKey && !e.ctrlKey && !e.altKey && this.lassoSelectionEnabled && !isSelected) {
            if (this.canUseLasso(e)) {
                // clicked on item itself ... not a content so we can select items
                // PETR: disabled for now because of issues with D&D
                ret = false; // lasso is active
                let lvpos = getAbsPosRect(this.container);
                let headerHeight = this.getVirtualHeights().headerHeight;
                let offset = this.getScrollOffset();
                this._lassoSelectionStart = {
                    x: e.pageX - lvpos.left,
                    y: e.pageY - lvpos.top - headerHeight,
                    startingItemIndex: div ? div.itemIndex : (this.isGrouped ? -1 /* #19563 */ : this.itemCount /* #17763 */),
                    itemIndex: div ? div.itemIndex : (this.isGrouped ? -1 /* 19563 */ : this.itemCount /* #17763 */),
                    direction: 0,
                    offset: offset,
                    lvpos: lvpos,
                    headerHeight: headerHeight
                };
                window.handleCapture(this.container, (e) => {
                    if (this._lassoSelectionStart) {
                        if (!isChildOf(this.container, e.target)) {
                            // check mouse position ... when it's above LV, select top visible item, when below LV, select bottom visible item
                            let rect = this.canvas.getBoundingClientRect();
                            let lvpos = {
                                top: rect.top + this._parentScrollTop,
                                left: rect.left,
                                bottom: rect.top + this._parentScrollTop + this.container.clientHeight
                            };
                            //var lvpos = getAbsPosRect(this.viewport);
                            if (e.clientY < lvpos.top) {
                                this.handleLassoMove(this.getDiv(this.firstVisibleItem), e);
                            }
                            else if (e.clientY > lvpos.bottom) {
                                this.handleLassoMove(this.getDiv(this.lastVisibleItem), e);
                            }
                        }
                    }
                }, (e) => {
                    this._cleanUpLasso();
                });
                if (div)
                    this.handleItemMouseOver(div, e);
                window.showSelectionLayer(true);
                this.updateLassoLayer(this._lassoSelectionStart.x, this._lassoSelectionStart.y, this._lassoSelectionStart.x, this._lassoSelectionStart.y);
            }
        }
        return ret;
    }
    updateLassoLayer(fromX, fromY, toX, toY) {
        window.updateLassoPosition(this.viewport, fromX, fromY, toX, toY);
    }
    _cleanUpLasso() {
        window.showSelectionLayer(false);
        // reset mouse selection
        this._lassoSelectionStart = undefined;
        this._lassoRangeStart = undefined;
        this._lassoRangeEnd = undefined;
        this._lastLassoUsageTm = Date.now();
    }
    updateLassoInfo(currentMouseInfo) {
    }
    handleLassoMove(div, e) {
        if (this.selectionMode)
            return;
        if (this._lassoSelectionStart) {
            let scrollRequireSum = 0;
            let offset = this.getScrollOffset();
            let lvPos = null;
            if (this.dynamicSize) {
                lvPos = getAbsPosRect(this.container);
                this._lassoSelectionStart.lvpos = lvPos;
            }
            let currentMouseInfo = {
                x: e.pageX - this._lassoSelectionStart.lvpos.left,
                y: e.pageY - this._lassoSelectionStart.lvpos.top - this._lassoSelectionStart.headerHeight,
                itemIndex: div ? div.itemIndex : -1 /*this.itemCount*/,
                offset: offset
            };
            // scroll only when user move with mouse
            if ((currentMouseInfo.x !== this._lassoSelectionStart.x) || (currentMouseInfo.y !== this._lassoSelectionStart.y)) {
                this.updateLassoInfo(currentMouseInfo);
                if (!this.dynamicSize)
                    lvPos = getAbsPosRect(this.container);
                let lvPosTop = (this.dynamicSize) ? (offset - Math.abs(lvPos.top)) : lvPos.top;
                let lvViewportHeight = ((this.dynamicSize) ? (this._parentOffsetHeight - this._lassoSelectionStart.headerHeight) : this.canvasHeight);
                let posY = (e.clientY - lvPosTop) - this._lassoSelectionStart.headerHeight;
                if ((posY < this.lassoAutoScrollOffset) && (offset > 0)) {
                    scrollRequireSum = -((this.lassoAutoScrollOffset - posY) * 3);
                }
                else if ((posY > lvViewportHeight - this.lassoAutoScrollOffset) && (offset < this.viewportSize - lvViewportHeight)) {
                    scrollRequireSum = (this.lassoAutoScrollOffset - (lvViewportHeight - posY)) * 3;
                }
                if (scrollRequireSum !== 0) {
                    this.setScrollOffset(offset + scrollRequireSum);
                    if (this.dynamicSize)
                        this._lassoSelectionStart.y += scrollRequireSum;
                }
            }
            // TODO: draw rectangle and enum items inside (for grids)
            // for now, simple from-to range selection (for lists)
            if (!this.isGrid) {
                let rangeStart = Math.min(this._lassoSelectionStart.itemIndex, currentMouseInfo.itemIndex);
                let rangeEnd = Math.max(this._lassoSelectionStart.itemIndex, currentMouseInfo.itemIndex);
                if ((rangeStart !== this._lassoRangeStart) || (rangeEnd !== this._lassoRangeEnd)) {
                    this._lassoRangeStart = rangeStart;
                    this._lassoRangeEnd = rangeEnd;
                    if (this._selectPromise) {
                        cancelPromise(this._selectPromise);
                        this._selectPromise = undefined;
                    }
                    if (this.dataSource && this.dataSource.selectRangeAsync && (rangeStart >= 0) && (rangeEnd >= 0))
                        this._selectPromise = this.dataSource.selectRangeAsync(rangeStart, rangeEnd, true, !e.ctrlKey && !e.shiftKey /* do clear selection */);
                }
            }
            this.updateLassoLayer(this._lassoSelectionStart.x - (this.isHorizontal ? offset - this._lassoSelectionStart.offset : 0), this._lassoSelectionStart.y - (this.isHorizontal ? 0 : offset - this._lassoSelectionStart.offset), currentMouseInfo.x, currentMouseInfo.y);
        }
    }
    handleItemMouseMove(div, e) {
        if (this.disabled)
            return;
        this.handleLassoMove(div, e);
    }
    handleItemMouseOver(div, e) {
        if (this.disabled)
            return;
        this.handleLassoMove(div, e);
    }
    handleItemMouseUp(div, e) {
        if (!this._isTreeView) // #18097
            e.stopPropagation(); // needed when LV is inside of LV (e.g. popups in artist grid)
        let handleSelection = this._lassoSelectionStart === undefined;
        this._cleanUpLasso();
        if (this.dndEventsRegistered)
            this.makeDraggable(div, true);
        if (this.disabled)
            return;
        if (handleSelection) {
            if (e.shiftKey || e.ctrlKey || (e.button !== 0) || !this.multiselect || (usingTouch && this.selectionMode))
                return;
            let ds = this._dataSource;
            if (ds) {
                ds.modifyAsync(() => {
                    let index = div.itemIndex;
                    if ((index < ds.count) && (index >= 0)) {
                        ds.clearSelection();
                        ds.setSelected(index, true);
                        this.raiseItemSelectChange(index);
                    }
                }, { onlyFlags: true });
            }
        }
    }
    showDelayedPopup(idx) {
        if (this._openingPopupTimer)
            clearTimeout(this._openingPopupTimer);
        this._openingPopupTimer = this.requestTimeout(() => {
            this._openingPopupTimer = undefined;
            this.showPopup(idx);
        }, this.gridPopupDelay); // #17584
    }
    handleItemClick(div, e) {
        if (this.disabled)
            return;
        let isLeftButton = (e.button == 0);
        if (isLeftButton) {
            if (e.shiftKey || e.ctrlKey) {
                this.closePopup();
                if (this.automaticSelectionMode)
                    this.selectionMode = true;
            }
            else {
                if (this._openingPopupTimer) {
                    clearTimeout(this._openingPopupTimer);
                    this._openingPopupTimer = undefined;
                }
                else if (this.popupSupport && (div.itemIndex !== undefined) && (!this.selectionMode)) {
                    this.showDelayedPopup(div.itemIndex);
                }
            }
        }
        if (isLeftButton && !e.shiftKey && !e.ctrlKey) {
            let item = this.getItem(div.itemIndex);
            if (item)
                this.raiseEvent('itemclick', {
                    item: item,
                    div: div,
                });
        }
    }
    handleItemDblClick(div, e) {
        if (this.disabled)
            return;
        if (this._openingPopupTimer) {
            clearTimeout(this._openingPopupTimer);
            this._openingPopupTimer = undefined;
        }
        this.closePopup();
        let item = this.getItem(div.itemIndex);
        if (item)
            this.raiseEvent('itemdblclick', {
                item: item,
                div: div
            });
    }
    invalidateAll() {
        this.invalidateNeeded = true;
        this.deferredDraw();
    }
    rebind() {
        this.forceRebindAll = true;
        this.deferredDraw();
    }
    handleItemInsert(itemIndex, obj) {
        if (itemIndex >= this.firstCachedItem + this.divs.length && itemIndex > this.lastVisibleItem + 1 /*possibly a new item to be drawn*/) { // This is below all items we cache, let's just update scrollbars, don't do anything else
        }
        else if (itemIndex < this.firstVisibleItem) {
            if (this.isGrid)
                this.invalidateAll();
            else {
                this.canvas.scrollTop += this.itemHeight + this.itemRowSpacing; // Make sure the same items remain visible after scrollbar update
                this.canvasScrollTop = this.canvas.scrollTop;
            }
        }
        else {
            this.invalidateAll();
        }
    }
    handleItemModify(itemIndex, obj) {
        if (itemIndex >= this.firstCachedItem && itemIndex < this.firstCachedItem + this.divs.length) {
            let div = this.getDiv(itemIndex);
            if (div) {
                div.forceRebind = true;
                this.deferredDraw();
            }
        }
    }
    handleItemDelete(itemIndex, obj) {
        if (itemIndex == this.focusedIndex || itemIndex === undefined /* #18750 */)
            this.setSelectedIndex(this.focusedIndex, true);
        this.invalidateAll(); // TODO: Re-introduce animated delete...
        return;
        /*    this.setItemCount(this.itemCount - 1);

            var div = this.deleteDiv(itemIndex);
            if (div) {
                if (itemIndex >= this.firstVisibleItem && itemIndex <= this.lastVisibleItem) {
                    // The deleted item is currently visible - animate the deletion
                    var itemDeleted = function (e) {
                        var div = e.currentTarget;
                        if (div.hasAttribute('data-deleting')) {
                            div.parentListView.hideDiv(div); // Hide this item
                            div.itemIndex = undefined;
                            div.parentListView.divs.push(div); // Let the item be re-used in our cache
                            div.removeAttribute('data-deleting'); // Remove deleting status
                            div.removeAttribute('data-run');
                            div.classList.remove('deleteitem');
                            app.unlisten(div, transitionEndEventName, itemDeleted);
                        }
                    };

                    app.listen(div, transitionEndEventName, itemDeleted);
                    div.classList.add('deleteitem');
                    div.setAttribute('data-deleting', '1');
                    div.setAttribute('data-run', '1');

                    this.animateNextDraw = true;
                    this.draw();
                } else {
                    div.itemIndex = undefined; // Invalidate the item
                    this.hideDiv(div);
                    this.divs.push(div); // Let the item be re-used in our cache
                }
            }

            if (itemIndex < this.firstVisibleItem) { // We have to redraw - the deleted item can have an impact on the visible items
                // TODO: Adjust scrollbars in case we can continue showing the very same content (just shift offset)
                this.invalidateAll();
            }*/
    }
    handleItemChange(eventType, itemIndex, obj, flags, flagData, flagValue) {
        let _this = this;
        if ((flags === 'flagchange') && (flagData === 1 /* selected */)) {
            // no need to invalidate all ... just refresh selection state
            this.forceRebindSelection = true;
            this.deferredDraw();
        }
        else {
            // Update pop-up location (and close it if necessary)
            if (this.popupDiv && this.dataSource && this.dataSource.indexOfPersistentIDAsync) {
                let shownIndex = _this.popupDiv.itemIndex;
                this.dataSource.indexOfPersistentIDAsync(this.popupDiv.itemID).then(function (idx) {
                    if (idx < 0)
                        _this.closePopup();
                    else if (_this.popupDiv && !_this.selectionMode && idx != _this.popupDiv.itemIndex && shownIndex == _this.popupDiv.itemIndex) {
                        _this.showPopup(idx);
                    }
                });
            }
            if (fullLVDebug)
                ODS('ListView.handleItemChange: ' + this.constructor.name + ' - ' + eventType);
            if ((this.isGrouped || this.checkGroups) && (eventType != 'modify'))
                this.invalidateAll(); // when grouped we need to recreate groups
            if (!eventType || (eventType === 'newcontent') || (eventType === 'autoupdate' /* #17483 */)) {
                // change event
                this.invalidateAll();
            }
            else {
                switch (eventType) {
                    case 'delete':
                        this.handleItemDelete(itemIndex, obj);
                        break;
                    case 'insert':
                        this.handleItemInsert(itemIndex, obj);
                        break;
                    case 'modify':
                        this.handleItemModify(itemIndex, obj);
                        break;
                }
            }
            this.requestFrame(() => {
                this.raiseSelectionChanged(); // will update context buttons in parent multiview, if needed
            }, 'raiseSelectionChanged');
        }
    }
    /**
    Sets the datasource and persist parameters of the previous view (i.e. the same selection, focused item, etc.)

    @methods setDataSourceSameView
    @param {Object} datasource Datasource object
    @param {bool} [forceRestoreFocus] If false, does not force re-setting focusedIndex after copying selection. Default true.
    */
    setDataSourceSameView(ds, forceRestoreFocus) {
        if (forceRestoreFocus === undefined) {
            if (this.forceRestoreFocus === undefined)
                forceRestoreFocus = true;
            else
                forceRestoreFocus = this.forceRestoreFocus;
        }
        // JH: TODO: currently we persist selection and focus, but possibly the top item (or some scrolling similar to the previous datasource) would be nice as well
        if (this._settingDSPromise) {
            cancelPromise(this._settingDSPromise);
            this._settingDSPromise = undefined;
        }
        if (ds && this._dataSource && !this.isFiltered()) {
            let oldIndex = this._dataSource.focusedIndex;
            if (forceRestoreFocus && (oldIndex > 0))
                ds.focusedIndex = -1; // in order to find later whether the focusedIndex was set in ds.copySelectionAsync() or not
            this.clearFilterSource();
            let copySelPromise = undefined;
            this._settingDSPromise = new Promise((resolve, reject) => {
                copySelPromise = ds.copySelectionAsync(this._dataSource);
                copySelPromise.then((firstSelectedIdx) => {
                    copySelPromise = undefined;
                    this.dataSource = ds;
                    if (forceRestoreFocus && (oldIndex > 0) && (ds.focusedIndex < 0) /* was not set in ds.copySelectionAsync() above */ && (oldIndex < ds.count)) {
                        ds.focusedIndex = oldIndex; // LS: e.g. when a track is deleted from playlist then we want to persist the focused index
                        ds.modifyAsync(() => {
                            if ((oldIndex < ds.count) && (oldIndex >= 0)) {
                                ds.setSelected(oldIndex, true);
                                this.raiseItemSelectChange(oldIndex);
                            }
                        }, { onlyFlags: true });
                    }
                    resolve(firstSelectedIdx);
                });
            });
            this._settingDSPromise.onCanceled = function () {
                if (copySelPromise) {
                    cancelPromise(copySelPromise);
                    copySelPromise = undefined;
                }
            };
            return this._settingDSPromise;
        }
        else {
            this.clearFilterSource();
            this.dataSource = ds;
            return dummyPromise();
        }
    }
    /**
    Whether to show header.

    @property showHeader
    @type boolean
    */
    get showHeader() {
        return this._showHeader;
    }
    set showHeader(value) {
        this._showHeader = value;
        setVisibility(this.header, this._showHeader);
    }
    get showInline() {
        return this._showInline;
    }
    set showInline(value) {
        this._showInline = value;
        if (value)
            this.container.classList.add('showInline');
        else
            this.container.classList.remove('showInline');
    }
    get canScrollHoriz() {
        return this._canScrollHoriz;
    }
    set canScrollHoriz(value) {
        this._canScrollHoriz = value;
        if (value) {
            this.container.classList.add('canScrollHoriz');
            if (this.canvas)
                this.canvas.style.overflowX = '';
        }
        else {
            this.container.classList.remove('canScrollHoriz');
            if (this.canvas)
                this.canvas.style.overflowX = 'hidden';
        }
    }
    /**
    Header title to be shown (in case 'showHeader' property is set).

    @property headerTitle
    @type string
    */
    get headerTitle() {
        return this.headerItems.innerText;
    }
    set headerTitle(value) {
        this.headerItems.innerText = value;
    }
    get selectionMode() {
        return this._selectionMode;
    }
    set selectionMode(value) {
        if (this._selectionMode !== value) {
            this._selectionMode = value;
            this.raiseSelectionChanged();
            if (value) {
                this.container.setAttribute('data-selection-mode', '1');
                this.closePopup();
            }
            else
                this.container.removeAttribute('data-selection-mode');
            this.invalidateAll();
        }
    }
    get collapseSupport() {
        return this._collapseSupport;
    }
    set collapseSupport(value) {
        this._collapseSupport = value;
        if (!value) {
            this.groupDivs.forEach(function (div) {
                if (div._collapseMark) {
                    div._collapseMark.remove();
                    div._collapseMark = undefined;
                }
            });
        }
    }
    /**
    Gets/sets index of the focused item. In case there's a datasource, its focusedIndex property is modified.

    @property focusedIndex
    @type integer
    */
    get focusedIndex() {
        let retval = -1;
        let ds = this._dataSource;
        if (ds) {
            retval = ds.focusedIndex;
        }
        return retval;
    }
    set focusedIndex(value) {
        let ds = this._dataSource;
        if (ds) {
            ds.focusedIndex = value;
        }
    }
    /**
    Gets the focused item/object according to the current focusedIndex

    @property focusedItem
    @type object
    */
    get focusedItem() {
        return this.dataSource ? this.dataSource.focusedItem : undefined;
    }
    /**
    Gets/sets the datasource which is/will be shown

    @property dataSource
    @type object
    */
    get dataSource() {
        return this._dataSource;
    }
    set dataSource(ds) {
        if (this._dataSource == ds)
            return;
        if (this.inEdit) {
            this.editCancel();
        }
        if (!this._handleItemChange) {
            this._handleItemChange = this.handleItemChange.bind(this);
            this._handleFocusChanged = this.handleFocusChanged.bind(this);
            this._handleSortChanged = this.handleSortChanged.bind(this);
        }
        let events = {
            'change': this._handleItemChange,
            'focuschange': this._handleFocusChanged,
            'sorted': this._handleSortChanged,
        };
        let oldDataSource = this._dataSource;
        if (this._dataSource) {
            this.cancelAutoSort();
            let oldds = this._dataSource;
            if (this.reportStatus)
                this.unregisterStatusBarSource(this._dataSource);
            for (let prop in events) {
                app.unlisten(oldds, prop, events[prop]);
            }
            this.cancelItemLoadingPromises();
            if (!this.forbiddenWhenLoadedCancel)
                cancelPromise(this._dataSource.whenLoaded());
            this.cleanUpPromises();
            this.closePopup(); // have to be called with non-empty data source
            this.selectionMode = false;
            if (this._updatesSuspended) {
                if (this._interactionTimeout)
                    clearTimeout(this._interactionTimeout);
                this._userInteractionDone();
            }
            this._dataSource = null;
            this.clearFilterSource();
            this.forceItemCountUpdate = true; //Otherwise datasource with the same # of items wouldn't be updated property in setItemCount()
        }
        this._dataSource = ds;
        this._fastObject = undefined; // LS: dataSource is changed, clear also cached _fastObject (passed through getFastObject() when binding data)
        this._fastObject2 = undefined;
        this.groupHeight = -1; // reset group size, so ti will be computed again for the new data source
        if (this._dataSource) {
            if (this.reportStatus)
                this.registerStatusBarSource(this._dataSource);
            for (let prop in events) {
                app.listen(this._dataSource, prop, events[prop]);
            }
        }
        let evt;
        evt = createNewCustomEvent('datasourcechanged', {
            detail: {
                newDataSource: this._dataSource,
                oldDataSource: oldDataSource
            },
            bubbles: true,
            cancelable: true
        });
        this.container.dispatchEvent(evt);
        let doRefresh = false;
        if (this._dataSource) {
            doRefresh = !this.forceAutoSort();
        }
        else
            doRefresh = true;
        if (doRefresh)
            this.invalidateAll();
    }
    calcPixsPerItems(itemCount) {
        return Math.max((this.showRowCount || Math.ceil(itemCount / this.itemsPerRow)) * (this.rowDimension + this.itemRowSpacing) - this.itemRowSpacing, 0);
    }
    getNextGroup(group) {
        let usePositionIndex = group.positionIndex !== undefined;
        return this.getItemGroup(usePositionIndex ? group.positionIndex + 1 : (group.index + group.itemCount), usePositionIndex);
    }
    getPrevGroup(group) {
        let usePositionIndex = group.positionIndex !== undefined;
        return this.getItemGroup(usePositionIndex ? group.positionIndex - 1 : (group.index - 1), usePositionIndex);
    }
    getItemGroup(itemIndex, usePositionIndex) {
        if (!this.dataSource)
            return undefined;
        return this.dataSource.getItemGroup(itemIndex, usePositionIndex);
    }
    getOffsetGroup(offset) {
        if (!this.isGrouped || !this.dataSource || !this.dataSource.getGroupsCount())
            return undefined;
        return this.dataSource.getOffsetGroup(offset);
    }
    groupsRecompute(reGroup, reComputeViewport, invalidateItemHeight) {
        return new Promise((resolve, reject) => {
            if (this._recomputePromise)
                cancelPromise(this._recomputePromise);
            if (!this.dynamicSize)
                this.saveRealScroll();
            this._restoreScrollPos = true;
            let loader = this.prepareGroupsAsync(reGroup);
            loader.then1((done) => {
                let reload = () => {
                    this.itemHeightReset = invalidateItemHeight;
                    this._adjustSizeNeeded = true;
                    this._groupsRefresh = true;
                    this._reComputeViewport = reComputeViewport;
                    this.invalidateAll();
                };
                if (loader.canceled) {
                    reject();
                    return;
                }
                this._recomputePromise = undefined;
                this.isGrouped = done;
                if (done === true) {
                    reload();
                }
                resolve(done);
            });
            this._recomputePromise = loader;
        }); // call in advance, can change isGrouped property based on result groups
    }
    _adjustSize() {
        let itemHeightReset = this.itemHeightReset;
        if ((this.isGrouped || this.checkGroups) && (!this._groupsRefresh))
            this.groupsRecompute(false, false, false);
        if ((this.itemHeight <= 0) || itemHeightReset) {
            let div = undefined;
            this.itemHeightReset = false;
            let newDiv = false;
            if (this.divs.length > 0) {
                div = this.divs[0];
                let i = 1;
                while ((!div || !div.isVis) && (i < this.divs.length)) {
                    div = this.divs[i];
                    i++;
                }
            }
            let tempVisible = false;
            if (!div) {
                div = this.createDiv();
                newDiv = true;
            }
            else if (!div.isVis) { // we already have first div, but not visible, make it temporary visible to compute correct height
                tempVisible = true;
                div.style.display = '';
            }
            // #17880 JL: Reset manually set style so we can get computed style from CSS only (and not our cached width & height values)
            if (this._refreshItemBoxProperties) {
                div.style.width = '';
                div.style.height = '';
                div.style.paddingLeft = '';
                div.style.paddingRight = '';
                this._refreshItemBoxProperties = false;
            }
            this.itemHeight = div.clientHeight;
            this.itemWidth = div.clientWidth;
            let cs = getComputedStyle(div);
            this.itemBoxProperties.width = Math.round(getPixelSize(cs.width, 'width', div));
            this.itemBoxProperties.height = Math.round(getPixelSize(cs.height, 'height', div));
            this.itemBoxProperties.paddingLeft = Math.round(getPixelSize(cs.paddingLeft, 'paddingLeft', div));
            this.itemBoxProperties.paddingRight = Math.round(getPixelSize(cs.paddingRight, 'paddingRight', div));
            if (this.isHorizontal) {
                this.rowDimension = this.itemWidth;
                this.colDimension = this.itemHeight;
            }
            else {
                this.rowDimension = this.itemHeight;
                this.colDimension = this.itemWidth;
            }
            if (newDiv) {
                this.divs.push(div);
                this.hideDiv(div);
            }
            else if (tempVisible) {
                div.style.display = 'none';
            }
        }
        let recomputeRequired = false;
        if ((this.groupHeight <= 0) || itemHeightReset) {
            if (this.isGrouped && this.groupHeaders) {
                let _div = this.groupDivs[0];
                if (_div === undefined) {
                    _div = this.createGroupDiv();
                    this.groupDivs.push(_div);
                    this.hideGroupDiv(_div);
                }
                let oldValue = this.groupHeight;
                if (!this._groupsRefresh)
                    this.groupHeight = _div.clientHeight;
                if (this.isHorizontal) {
                    this.colGroupDimension = this.groupHeight;
                }
                else {
                    this.colGroupDimension = _div.clientWidth;
                }
                recomputeRequired = oldValue !== this.groupHeight;
            }
            else {
                this.groupHeight = 0;
                this.colGroupDimension = 0;
            }
        }
        if ((this.groupSepHeight <= 0) || itemHeightReset) {
            if (this.isGrouped && this.groupSeparators) {
                let oldValue = this.groupSepHeight;
                let divG = this.groupSepDivs[0];
                if (divG === undefined) {
                    divG = this.createGroupSepDiv();
                    this.groupSepDivs.push(divG);
                    this.hideGroupSepDiv(divG);
                }
                this.groupSepHeight = divG.clientHeight;
                recomputeRequired = recomputeRequired || (oldValue !== this.groupSepHeight);
                if (!recomputeRequired && this.groupSepHeight === 0) { // groupSeparators is true, but groupSepHeight is still zero .. let's plan to compute again
                    this.requestFrame(() => {
                        this.adjustSize();
                    }, 'adjustSize');
                }
            }
            else {
                this.groupSepHeight = 0;
            }
        }
        if (recomputeRequired)
            this.groupsRecompute(false, true, false);
        let origscroll = 0;
        let rect = this.getVisibleRect();
        rect.width = this.canvasWidth; // this differs when scrollingParent is defined
        if (fullLVDebug)
            ODS('**** adjustSize called for: ' + this.itemCount + ', height: ' + rect.height + ', width: ' + rect.width + ', lvWidth: ' + this.container.offsetWidth + ', uniqueId = ' + this.uniqueID);
        if (!this.isGrid) { // Always update item width to full control width in case of a simple list view
            this.itemWidth = rect.width;
            this.colDimension = rect.width;
        }
        let w, h;
        let itemColDim, itemRowDim;
        if (this.isHorizontal) {
            w = rect.height;
            h = rect.width;
            itemColDim = this.itemHeight;
            itemRowDim = this.itemWidth;
        }
        else {
            w = rect.width;
            h = rect.height;
            itemColDim = this.itemWidth;
            itemRowDim = this.itemHeight;
        }
        if (this.isGrid) {
            this.itemsPerRow = Math.floor((w - itemColDim - this.colGroupDimension) / (itemColDim + this.itemHorzSpacing)) + 1;
            if (this.itemsPerRow < 1)
                this.itemsPerRow = 1;
        }
        else {
            this.itemsPerRow = 1;
        }
        if (this.itemsPerRow > 1 && this.distributeEmptySpace)
            this.itemRedistSpacing = (w - this.colGroupDimension - (this.itemsPerRow * itemColDim) - ((this.itemsPerRow - 1) * this.itemHorzSpacing)) / this.itemsPerRow;
        else
            this.itemRedistSpacing = 0;
        // Make sure the cache is large enough to accomodate all pre-drawn screens
        this.divsPerScreen = Math.floor(h / Math.max(itemRowDim, 1) + 1) * this.itemsPerRow;
        this.maxCachedDivs = Math.max(this.minCachedDivs, this.divsPerScreen * (1 /*just the visible screen*/ + 2 * this.preDrawAmount));
        // Adjust background div size
        let size = 0;
        if (this.isGrouped && this.dataSource) {
            size = this.dataSource.getGroupsSize();
            if (!size) { // groups are not prepared yet
                size = this.calcPixsPerItems(this.itemCount);
            }
        }
        else {
            size = this.calcPixsPerItems(this.itemCount);
        }
        // Clean all skips start, so that it's correctly recalculated in the loop below
        for (let i = 0; i < this.skips.length; i++)
            this.skips[i]._startPx = Number.MAX_SAFE_INTEGER;
        // Make sure skips are sorted according to item indexes
        this.skips.sort(function (o1, o2) {
            return o1.afterIndex - o2.afterIndex;
        });
        // Add reserved space for skips
        let targetSizeDiff = 0;
        for (let i = 0; i < this.skips.length; i++) {
            let skip = this.skips[i];
            if (skip.afterIndex < this.itemCount) {
                skip._startPx = this.getItemTopOffset(skip.afterIndex) + this.rowDimension;
                size += skip.reservePx;
                if (skip.targetPx !== undefined && !(skip.mix && skip.hide)) {
                    targetSizeDiff += (skip.targetPx - skip.reservePx);
                }
            }
            else
                skip._startPx = Number.MAX_SAFE_INTEGER;
        }
        let result = true;
        if (!this._groupsRefresh || this._reComputeViewport || (!this.viewportSize) || (!this.viewportSizeY)) { // do not recompute viewport size after groups refresh (otherwise it can stuck in infinite loop due notifyChange called by setViewportSize)    
            let rW = this.requiredWidth();
            if ((size != this.getViewportSize()) || ((this.viewportSizeY != rW) && (rW > 0))) {
                if (fullLVDebug)
                    ODS('LV: Setting new viewport size: ' + size + '/' + rW + ', uniqueId = ' + this.uniqueID);
                let currentWidth = this.getVisibleColsDim();
                // @ts-ignore
                this.container.targetOffsetHeight = (targetSizeDiff ? size + targetSizeDiff : undefined); // So that other controls know our _intended_ size (after animation ends)
                this.setViewportSize(size, rW);
                this.getCanvasSizeAndPos(false /*not cached in order to force refresh of its values*/);
                this.parentScrollFrame(); // Size changes can cause scroll changes that we need to apply.
                result = (currentWidth == this.getVisibleColsDim());
            }
        }
        // Adjust visible viewport (i.e. canvas without scrollbars)
        if (this.canvasWidth > 0) {
            if (fullLVDebug)
                ODS('LV: Setting new viewport width: ' + this.canvasWidth + ', lvWidth: ' + this.container.offsetWidth + ', uniqueId = ' + this.uniqueID);
            if (this.ignoreReflowOptimizations) {
                this.viewport.style.height = setPix(this.canvasHeight);
                this.viewport.style.width = setPix(this.canvasWidth);
            }
            else {
                applyStylingAfterFrame(() => {
                    if (!this._cleanUpCalled) {
                        this.viewport.style.height = setPix(this.canvasHeight);
                        this.viewport.style.width = setPix(this.canvasWidth);
                    }
                });
            }
        }
        this._groupsRefresh = false;
        this._reComputeViewport = false;
        return result;
    }
    adjustSize(adjustItems) {
        if (!this.visible) {
            this._adjustSizeNeeded = true; // Adjust it later, when we are back visible
            return;
        }
        this._adjustSizeNeeded = false;
        if (adjustItems)
            this.resizeDivs(this.container.offsetWidth, this.container.offsetHeight);
        if (!this._adjustSize())
            this._adjustSize(); // JH: This recalc is needed when a scrollbar is shown/hidden by the setViewportSize() call above	
    }
    setViewportSize(size, sizeY) {
        if ((this.viewportSize != size) || (sizeY !== undefined)) {
            if (this.viewportSize != size) {
                this.viewportSize = size;
                this.dummy.style[this.isHorizontal ? 'width' : 'height'] = setPix(size);
                this.scrollingCanvas.style[this.isHorizontal ? 'width' : 'height'] = setPix(size);
            }
            if (this.viewportSizeY != sizeY) {
                this.viewportSizeY = sizeY;
                this.dummy.style[this.isHorizontal ? 'height' : 'width'] = setPix(sizeY);
                this.scrollingCanvas.style[this.isHorizontal ? 'height' : 'width'] = (sizeY < this.canvasWidth ? '100%' : setPix(sizeY));
            }
            this.onSizeChanged(size);
            if (this.dynamicSize && this.scrollingParent) {
                idleNotifyLayoutChangeDown(this.scrollingParent); // Notify all children of our parent scrolling element
            }
        }
    }
    getViewportSize() {
        return this.viewportSize;
    }
    getCanvasSizeAndPos(cached) {
        if (!cached || !this.canvasWidth) {
            this.canvasWidth = this.canvas.clientWidth;
            this.canvasHeight = this.canvas.clientHeight;
            if (this.headerFill) {
                if (this.canvas.scrollHeight > this.canvas.clientHeight) {
                    // move header by scrollbar width to the left, so it will not lose aligning
                    if (!this._headerFillPaddingSet) {
                        this.headerFill.style.paddingRight = getScrollbarWidth() + 'px';
                        this._headerFillPaddingSet = true;
                    }
                }
                else {
                    if (this._headerFillPaddingSet) {
                        this.headerFill.style.paddingRight = '';
                        this._headerFillPaddingSet = false;
                    }
                }
            }
        }
        return {
            w: this.canvasWidth,
            h: this.canvasHeight,
            l: this.canvasScrollLeft,
            t: this.canvasScrollTop
        };
    }
    getVisibleRect() {
        if (this.dynamicSize) { // TODO: This isn't yet implemented for virtual horizontal scrolling
            let parent = this.scrollingParent;
            if (parent) {
                let h = Math.min(this._parentOffsetHeight, this._containerOffsetTop - this._parentScrollTop + this._containerOffsetHeight) - Math.max(this._containerOffsetTop - this._parentScrollTop, 0) - this._headerOffsetHeight;
                if (h < 0)
                    h = 0;
                if (fullLVDebug)
                    ODS('*** LV.getVisibleRect: this._parentScrollTop = ' + this._parentScrollTop + ', this._containerOffsetTop = ' + this._containerOffsetTop + ', uniqueId = ' + this.uniqueID);
                return {
                    top: Math.max(this._parentScrollTop - this._containerOffsetTop, 0),
                    height: h,
                    width: parent.offsetWidth // truly visible part in scroller (needed because of #15382, #15427)
                };
            }
        }
        return {
            top: this.getSmoothScrollOffset(),
            height: this.canvasHeight,
            width: this.canvasWidth
        };
    }
    setItemCount(cnt) {
        if (cnt != this.itemCount || this.forceItemCountUpdate) {
            this.recalcLayoutNeeded = true;
            if (fullLVDebug)
                ODS('*** Changed item count from ' + this.itemCount + ' to ' + cnt + ', uniqueId = ' + this.uniqueID);
            this.itemCount = cnt;
            if (this.visible) {
                this.forceItemCountUpdate = false;
                this.adjustSize(false);
            }
            else {
                if (fullLVDebug)
                    ODS('***Invisible LV, recalcLayout will be needed later');
                this.invalidateNeeded = true;
                this.forceItemCountUpdate = true;
            }
            if (this.horLineSepDiv)
                setVisibilityFast(this.horLineSepDiv, cnt > 0);
        }
    }
    handleBinding(div, index) {
        if (this._dataSource) {
            let _this = this;
            this._dataSource.locked(function () {
                _this.handleBinding_locked(div, index);
            });
        }
        else
            this.handleBinding_locked(div, index);
    }
    handleBinding_locked(div, index) {
        if (div && this._dataSource) {
            let rebind = (div.itemIndex != index);
            if (rebind)
                div.itemIndex = index;
            if (this._dataSource)
                this.markSelected(div, this._dataSource.isSelected(index));
            let focused = (this.focusedIndex == index);
            this.markFocused(div, focused);
            rebind = rebind || div.forceRebind;
            div.forceRebind = false;
            if (rebind) {
                this.cancelItemLoadingPromise(div);
                let bindObj;
                if (this.useFastBinding) {
                    this._fastObject = this.dataSource.getFastObject(index, this._fastObject);
                    bindObj = this._fastObject;
                }
                else {
                    bindObj = this.dataSource.getValue(index);
                }
                this.bindData(div, index, bindObj);
                if (!this.isGrid && !this.noItemOverstrike) {
                    if ((index & 1) === 0)
                        div.setAttribute('data-even', '1');
                    else
                        div.removeAttribute('data-even');
                }
            }
        }
    }
    markSelected(div, selected) {
        if (selected && !this.noItemOverstrike) {
            div.setAttribute('data-selected', '1');
            setAriaActiveDescendant(div, this.container); // Screen reader support
        }
        else {
            div.removeAttribute('data-selected');
            clearAriaID(div); // Screen reader support
        }
    }
    focusRefresh(newFocusState) {
        this.focusVisible = newFocusState;
        if ((newFocusState) && (this.focusedIndex == -1) && (isUsingKeyboard()) && (this._dataSource && this._dataSource.count) && (!this.getScrollOffset() /* not scrolled */)) {
            // PETR: make first item focused when navigated by TAB and nothing is selected/focused
            this.focusedIndex = 0;
        }
        let div = this.getDiv(this.focusedIndex);
        if (div) {
            this.markFocused(div, newFocusState);
        }
    }
    markFocused(div, focused) {
        if (div.hasAttribute('data-focused') !== focused)
            div.forceRebind = true;
        if (focused) {
            div.setAttribute('data-focused', '1');
            if (this.focusVisible)
                div.setAttribute('data-keyfocused', '1');
        }
        else {
            div.removeAttribute('data-focused');
            div.removeAttribute('data-keyfocused');
        }
    }
    addItemToCanvas(div) {
        this.viewport.appendChild(div);
    }
    stopPreDraw() {
        if (this._predrawTimeout) {
            clearTimeout(this._predrawTimeout);
            this._predrawTimeout = undefined;
            this.preDrawnScreens = 0;
        }
    }
    cancelItemLoadingPromises() {
        this.divs.forEach(function (div) {
            this.cancelItemLoadingPromise(div);
        }.bind(this));
    }
    cancelItemLoadingPromise(div) {
        if (div.loadingPromise && !div.loadingPromise.finished) {
            cancelPromise(div.loadingPromise);
            div.loadingPromise = undefined;
        }
    }
    clearDivs() {
        this.stopPreDraw();
        // Clean up all items/divs
        if (this.divs) {
            for (let i = 0; i < this.divs.length; i++) {
                let div = this.divs[i];
                if (div) {
                    this.cancelItemLoadingPromise(div);
                    this.cleanUpDiv(div);
                    if (div.parentNode)
                        removeElement(div);
                    div.parentListView = undefined;
                }
            }
            this.divs.length = 0;
        }
        // Clean up group headers
        if (this.groupDivs) {
            for (let i = 0; i < this.groupDivs.length; i++) {
                let div = this.groupDivs[i];
                if (div) {
                    this.cancelItemLoadingPromise(div);
                    this.cleanUpGroupHeader(div);
                    div.parentListView = undefined;
                    if (div._collapseMark) {
                        div._collapseMark.remove();
                    }
                    if (div.parentNode)
                        removeElement(div);
                }
            }
            this.groupDivs.length = 0;
        }
        // Clean up group separators
        if (this.groupSepDivs) {
            for (let i = 0; i < this.groupSepDivs.length; i++) {
                let div = this.groupSepDivs[i];
                this.cleanUpGroupSep(div);
                div.parentListView = undefined;
                if (div.parentNode)
                    removeElement(div);
            }
            this.groupSepDivs.length = 0;
        }
        this.firstCachedItem = 0;
    }
    getItem(index) {
        if (this._dataSource) {
            let result;
            this._dataSource.locked(function () {
                if (index >= 0 && index < this._dataSource.count)
                    result = this._dataSource.getValue(index);
            }.bind(this));
            return result;
        }
    }
    getFastItem(index) {
        if (this._dataSource) {
            let retval = undefined;
            this._dataSource.locked(function () {
                if (index >= 0 && index < this._dataSource.count) {
                    retval = this._dataSource.getFastObject(index, this._fastObject2);
                }
            }.bind(this));
            return retval;
        }
    }
    // used for in-place editing to get item for edit, by default it is the same as LV item
    getItemForEdit(index) {
        return this.getItem(index);
    }
    // ============== Methods below are to be overriden in descendants in order to achieve desired behavior =================
    // Called just once to initialize the view
    setUpDiv(div) { }
    // Called often to bind the currently active data
    bindData(div, index, item) {
        if (this.bindFn)
            this.bindFn(div, item);
    }
    // Called on div that aren't currently being used (not visible to show data)
    suspendDiv(div) {
        // SVG animations are eating CPU even when they're hidden ... so remove all SVGs with any animation (#15258)
        // data-hasSVGAnimation property is set automatically in loadIcon when SVG contain animation
        let svgs = qes(div, '[data-hasSVGAnimation]');
        if (svgs) {
            for (let i = 0; i < svgs.length; i++) {
                svgs[i].remove();
            }
            div.loadedIcon = undefined;
            return true;
        }
        else {
            return false;
        }
    }
    // Called in the end to clean up anything registered by the div
    cleanUpDiv(div) {
        if (div.unlisteners) {
            forEach(div.unlisteners, function (unlistenFunc) {
                unlistenFunc();
            });
            div.unlisteners = undefined;
        }
    }
    // Called just once to initialize the group header
    setUpGroupHeader(div) { }
    // Called often to bind data to a group header
    renderGroupHeader(div, group, forceRebind) {
        div.innerText = group.id;
    }
    // Called in the end to clean up anything registered by the div (group header)
    cleanUpGroupHeader(div) {
        div.parentListView = undefined;
    }
    // Called just once to initialize the group separator
    setUpGroupSep(div) { }
    // Called often to bind data to a group header
    renderGroupSep(div, group) { }
    // Called in the end to clean up anything registered by the div (group header)
    cleanUpGroupSep(div) {
        div.parentListView = undefined;
    }
    setUpHeader(header) {
        header.classList.add('lvHeaderSingleItem');
    }
    // Called when d&d is finished
    dropToPosition(targetItemIndex) {
        if (this._dataSource) {
            this._dataSource.autoSort = false;
            this._dataSource.moveSelectionTo(targetItemIndex);
        }
    }
    // Called when render state is changed
    renderState(state) { }
    // Called often (after any modification) to get a list of all groups
    prepareGroupsAsync(reGroup) {
        return new Promise((resolve) => {
            if (this._dataSource && this._dataSource.prepareGroupsAsync) {
                this._dataSource.prepareGroupsAsync({
                    groupSepHeight: this.groupSepHeight,
                    groupSpacing: this.groupSpacing,
                    showRowCount: this.showRowCount,
                    itemsPerRow: this.itemsPerRow,
                    rowDimension: this.rowDimension,
                    itemRowSpacing: this.itemRowSpacing,
                    groupHeight: this.groupHeight,
                    regroup: reGroup && !this._regroupSuspended
                }).then1((done) => {
                    resolve.call(this, done);
                });
            }
            else
                resolve(false);
        });
    }
    requiredWidth(visibleWidth) {
        return undefined; // The default, which means that there's no specific width required, overriden e.g. in GridView
    }
    zoomIn() {
        //alert('Called Zoom In'); // commented as it's not yet implemented
    }
    zoomOut() {
        //alert('Called Zoom Out'); // commented as it's not yet implemented
    }
    onFocusChanged(newfocusedIndex) {
        if (!this.dontEmitFocusChange)
            this.raiseEvent('focuschange', {
                index: newfocusedIndex
            }, true, false /* LS: don't bubble*/);
    }
    onSizeChanged(newsize) {
        this.raiseEvent('sizechanged', {
            size: newsize
        }, true, true);
    }
    storeState() {
        if (!this.disableStateStoring && this.dataSource) {
            return {
                focusedIndex: this.focusedIndex,
                itemCount: this.dataSource.count,
                scrollOffset: this.getScrollOffset(),
                popupShown: this.isPopupShown()
            };
        }
        else
            return {};
    }
    resetState() {
        if (!this.dontResetState) {
            // LS: used when this control is added to controlCache to have the default values again
            this.setScrollOffset(0);
            this.resetScrollbars();
        }
    }
    restoreState(fromObject) {
        if (this.disableStateStoring)
            return;
        ODS('ListView.restoreState: ' + JSON.stringify(fromObject));
        let DS = this.dataSource;
        assert(DS, 'ListView.restoreState: dataSource unassigned !');
        DS.whenLoaded().then(() => {
            // dataSource is loaded, draw it and restore:
            let currentOffset = this.getScrollOffset();
            if (!currentOffset || (this.scrollingParent && fromObject.scrollOffset == currentOffset)) // If user hasn't scrolled manually yet
             {
                if (DS.count == fromObject.itemCount && fromObject.focusedIndex >= 0) {
                    this._requestFocusIndex = fromObject.focusedIndex;
                    ODS('ListView.restoreState: requested focused index: ' + fromObject.focusedIndex);
                    this._requestPopup = fromObject.popupShown;
                }
                this._requestScrollPosition = fromObject.scrollOffset;
                ODS('ListView.restoreState: requested scroll position: ' + fromObject.scrollOffset + ', DS.count = ' + DS.count);
                this.invalidateAll();
            }
            else {
                ODS('ListView.restoreState: user already scrolled manually to ' + currentOffset + ', restore offset is: ' + fromObject.scrollOffset);
            }
        });
    }
    createHeaderLayout() {
        this.container.classList.add('flex');
        this.container.classList.add('column');
        // 'header' element for a non-scrolling header
        this.header = document.createElement('div');
        this.header.style.height = 'auto';
        this.header.style.overflow = 'hidden';
        this.header.style.position = 'sticky';
        this.header.style.top = '0px';
        this.header.className = 'lvHeader';
        this.header.setAttribute('data-header', '1');
        this.container.appendChild(this.header);
        this.header.controlClass = new Control(this.header); // to allow assigning context menu
        // 'headerItems' element for the scrolling part of header
        this.headerItems = document.createElement('div');
        this.headerItems.style.height = 'auto';
        this.headerItems.style.overflow = 'hidden';
        this.headerItems.className = 'lvHeaderItems';
        this.header.setAttribute('data-headeritems', '1');
        this.header.appendChild(this.headerItems);
        this.setUpHeader(this.headerItems);
        setVisibility(this.header, this._showHeader);
        // 'body' element for the rest of LV, i.e. everything without a header
        this.body = document.createElement('div');
        this.body.style.overflow = 'hidden';
        this.body.className = 'lvBody fill';
        this.container.appendChild(this.body);
        // 'fill' is only here, so that we have an 'absolute' positioned parent, relatively to which all item divs will be positioned
        this.fill = document.createElement('div');
        this.fill.className = 'lvFill fill';
        this.body.appendChild(this.fill);
    }
    createItemsLayout() {
        // 'canvas' is a static positioned element, so that the descendant div items don't scroll with it. It shows scrollbars, when necessary.
        this.canvas = document.createElement('div');
        this.canvas.className = 'lvCanvas';
        this.canvas.style.height = '100%';
        this.canvas.style.width = '100%';
        this.canvas.style.overflow = this.noScroll ? 'hidden' : 'auto';
        if (!this.canScrollHoriz)
            this.canvas.style.overflowX = 'hidden';
        // 'viewport' is the main element where all the drawing occurs (and parent of all the divs)
        // It's the same as canvas, but dynamically scaled to not include canvas scrollbars and using 'overflow: hidden' it cuts all children divs to not be drawn over scrollbars.
        this.viewport = document.createElement('div');
        this.viewport.className = 'lvViewport';
        this.viewport.style.overflow = 'hidden';
        this.viewport.style.position = 'absolute';
        this.canvas.appendChild(this.viewport);
        // 'scrollingCanvas' element is here pretty much for possible drawing effects only - e.g. there can be a gradient background be drawn behind all item.
        this.scrollingCanvas = document.createElement('div');
        this.scrollingCanvas.className = 'lvScrollingCanvas';
        this.scrollingCanvas.style.height = '100%';
        this.scrollingCanvas.style.width = '100%';
        this.canvas.appendChild(this.scrollingCanvas);
        // 'Dummy' element makes sure that the horizontal and vertical scrollbars of 'canvas' have the correct dimensions
        this.dummy = document.createElement('div');
        this.dummy.style.height = '100%';
        this.dummy.style.width = '100%';
        this.scrollingCanvas.appendChild(this.dummy);
        this.fill.appendChild(this.canvas);
        /////////////////////////////
        // TOUCH SUPPORT 
        /////////////////////////////
        // get original offset from touch start 
        let getStartOffset = () => {
            if (this.isHorizontal)
                return this._originalTouchPos.screenX;
            else
                return this._originalTouchPos.screenY;
        };
        // get current touch position
        let getOffset = (e) => {
            if (this.isHorizontal)
                return e.touches[0].screenX;
            else
                return e.touches[0].screenY;
        };
        let getMaxSize = () => {
            return (this.isHorizontal ? this.scrollingCanvas.clientWidth - this.canvas.clientWidth : this.scrollingCanvas.clientHeight - this.canvas.clientHeight);
        };
        let translateMethod = () => {
            return (this.isHorizontal ? 'translateX' : 'translateY');
        };
        this._setGum = (isTouch, newPosition) => {
            if ((this._touchScroll && isTouch) || !isTouch) {
                let maxSize = getMaxSize();
                if (maxSize > 0) {
                    let gumSize = 0;
                    if (newPosition < 0)
                        gumSize = Math.abs(newPosition / 8);
                    else {
                        if (newPosition > maxSize)
                            gumSize = -Math.abs((newPosition - maxSize) / 8);
                    }
                    if (gumSize != 0) {
                        this._gumSize = gumSize;
                        this.viewport.style.transform = translateMethod() + '(' + gumSize + 'px)';
                    }
                    else {
                        this._gumSize = 0;
                        if (!isTouch)
                            this._lastOffset = undefined;
                    }
                    if (!isTouch) {
                        if (this._gumSize !== undefined && this._gumSize != 0) {
                            this.requestTimeout(() => {
                                this._lastOffset = undefined;
                                this._releaseGum();
                            }, 150, 'gumtimer');
                        }
                    }
                }
            }
        };
        this._releaseGum = () => {
            // Animate gum using Web Animations            
            this._gumSize = this._gumSize / 2;
            let atBeg = this._lastOffset <= 0;
            //this.viewport.style.transition = 'all 0.1s ease-out';
            this._gumplayer = this.viewport.animate([
                {
                    transform: this.viewport.style.transform
                },
                {
                    transform: translateMethod() + '(' + (this._gumSize * (atBeg ? -1 : 1)) + 'px)'
                },
                {
                    transform: translateMethod() + '(' + (this._gumSize / 2 * (!atBeg ? -1 : 1)) + 'px)'
                },
                {
                    transform: translateMethod() + '(' + (this._gumSize / 4 * (atBeg ? -1 : 1)) + 'px)'
                },
                {
                    transform: translateMethod() + '(' + (this._gumSize / 8 * (!atBeg ? -1 : 1)) + 'px)'
                },
                {
                    transform: translateMethod() + '(' + (this._gumSize / 16 * (atBeg ? -1 : 1)) + 'px)'
                },
                {
                    transform: translateMethod() + '(' + (this._gumSize / 32 * (!atBeg ? -1 : 1)) + 'px)'
                }
            ], {
                easing: 'ease-out',
                duration: 500
            });
            app.listen(this._gumplayer, 'finish', () => {
                app.unlisten(this._gumplayer);
                this.viewport.style.transform = '';
                this.viewport.style.transition = '';
                this._gumSize = undefined;
                this._gumplayer = undefined;
            });
            /*
            if (this._gumReleaseStep !== undefined)
                app.unlisten(this.viewport, transitionEndEventName, releaseGum);

            if (this._gumReleaseStep === undefined || this._gumReleaseStep < 6) {
                if (this._gumReleaseStep === undefined)
                    this._gumReleaseStep = 1;
                else
                    this._gumReleaseStep++;
                this._gumSize = this._gumSize / 4;
                this.viewport.style.transition = 'all 0.1s ease-out';
                this.viewport.style.transform = translateMethod() + '(' + (this._gumSize * (this._gumReleaseStep & 1 ? -1 : 1)) + 'px)';
                app.listen(this.viewport, transitionEndEventName, releaseGum);
            } else {
                this.viewport.style.transform = '';
                this._gumSize = undefined;
                this._gumReleaseStep = undefined;
            }*/
        };
        // scroll to new position
        let scrollTo = (newPosition) => {
            let _this = this;
            this.requestFrame(function () {
                _this._lastOffset = newPosition;
                _this.setScrollOffset(newPosition);
                _this._setGum(true, newPosition);
            }, 'setScrollOffset');
        };
        // compute velocity of the touch (how fast user moving)
        let computeVelocity = () => {
            let now = Date.now();
            let elapsed = now - this._lastTimestamp;
            this._lastTimestamp = now;
            let delta = this._lastOffset - this._lastComputeOffset;
            this._lastComputeOffset = this._lastOffset;
            let v = (900 * delta) / (elapsed);
            this._velocity = 0.8 * v + 0.2 * this._velocity;
        };
        // compute and scroll decelerated (when user moves quickly and releases touch)
        let deceleration = () => {
            if (this._deceleration && !this._touchScroll) {
                let elapsed = Date.now() - this._lastTimestamp;
                let delta = -this._deceleration * Math.exp(-elapsed / 350 /* total time of deceleration */);
                let newOffset = this._targetScrollOffset + delta;
                if (delta > 0.5 || delta < -0.5) {
                    scrollTo(newOffset);
                    this.requestFrame(deceleration, 'deceleration');
                }
                else {
                    scrollTo(this._targetScrollOffset);
                }
            }
        };
        let touchstart = (e) => {
            this._touchScroll = true;
            if (e.touches.length == 1) {
                this._velocity = 0;
                this._deceleration = 0;
                this._lastTimestamp = Date.now();
                this._lastTouchPos = e.touches[0];
                this._lastOffset = this.getScrollOffset();
                this._originalTouchPos = this._lastTouchPos;
                this._originalOffset = this._lastOffset;
                this._lastComputeOffset = this._lastOffset;
                this._gumSize = undefined;
                this._gumReleaseStep = undefined;
                this.viewport.style.transition = '';
                this.viewport.style.transform = '';
                clearInterval(this._touchTimer);
                this._touchTimer = setInterval(computeVelocity, 100);
                //e.preventDefault();
                //e.stopPropagation();
            }
        };
        let touchmove = (e) => {
            if (this._touchScroll) {
                if (e.touches.length == 1) {
                    let moveOffset = getOffset(e) - getStartOffset();
                    scrollTo(this._originalOffset - moveOffset);
                }
            }
        };
        let touchend = (e) => {
            this._touchScroll = false;
            clearInterval(this._touchTimer);
            computeVelocity();
            if (this._gumSize !== undefined && this._gumSize != 0) {
                this._releaseGum();
            }
            else if ((this._velocity > 10 || this._velocity < -10) && (!this._dynamicSize)) {
                this._deceleration = 0.8 * this._velocity;
                let maxSize = getMaxSize();
                this._targetScrollOffset = Math.min(maxSize, Math.max(0, Math.round(this._lastOffset + this._deceleration)));
                if (this.getScrollOffset() != this._targetScrollOffset) {
                    this._lastTimestamp = Date.now();
                    this.requestFrame(deceleration, 'deceleration');
                }
            }
            //e.preventDefault();
            //e.stopPropagation();
        };
        let _this = this;
        app.listen(this.viewport, 'touchstart', function (e) {
            touchstart(e);
        }, window.addPassiveOption(false));
        app.listen(this.viewport, 'touchmove', function (e) {
            touchmove(e);
        }, window.addPassiveOption(false));
        app.listen(this.viewport, 'touchend', function (e) {
            touchend(e);
        }, window.addPassiveOption(false));
        app.listen(this.viewport, 'touchcancel', function (e) {
            touchend(e);
        }, window.addPassiveOption(false));
    }
    _setGum(arg0, newPosition) {
        throw new Error('Method not implemented.');
    }
    invertCheckStateForSelected() {
        let ds = this._dataSource;
        ds.modifyAsync(function () {
            if (ds.count) {
                ds.beginUpdate();
                fastForEach(ds, function (item, index) {
                    if (ds.isSelected(index))
                        ds.setChecked(index, !ds.isChecked(index));
                });
                ds.endUpdate();
            }
        }.bind(this)).then(() => {
            this.invalidateAll();
            let event = createNewCustomEvent('checkedchanged', {
                detail: null,
                bubbles: true,
                cancelable: true
            });
            this.container.dispatchEvent(event);
        });
    }
    // internal
    headerContextMenuHandler(e) {
        if (this._headerContextMenu) {
            e.stopPropagation();
            let pos = window.getScreenCoordsFromEvent(e);
            this._headerContextMenu.show(pos.left, pos.top);
        }
    }
    contextMenuHandler(e) {
        e.stopPropagation();
        let _super_contextMenuHandler = super.contextMenuHandler.bind(this);
        whenAll(this._contextMenuPromises).then(() => {
            _super_contextMenuHandler(e);
        });
    }
    cleanUpPromises() {
        for (let ids = 0; ids < this._contextMenuPromises.length; ids++) {
            if ((this._contextMenuPromises[ids]) && (isPromise(this._contextMenuPromises[ids]))) {
                cancelPromise(this._contextMenuPromises[ids]);
            }
        }
        this._contextMenuPromises = [];
        super.cleanUpPromises();
    }
    // forces resort of the list and return true when resort is placed or false when auto sort not supported
    forceAutoSort() {
        if (this._autoSortString && this._dataSource && (this.autoSortSupported || this.canSaveNewOrder) && this._dataSource.setAutoSortAsync) {
            this._lastSorting = this._dataSource.setAutoSortAsync(this._autoSortString);
            this._lastSorting.then(() => {
                this._lastSorting = undefined;
                this.invalidateAll();
            });
            return true;
        }
        return false;
    }
    /**
    Gets/sets context menu of the header.

    @property headerContextMenu
    @type Menu
    */
    get headerContextMenu() {
        return this._headerContextMenu;
    }
    set headerContextMenu(value) {
        this._headerContextMenu = value;
        if (value && this._headerContextMenuHandler === undefined) {
            this._headerContextMenuHandler = this.headerContextMenuHandler.bind(this);
            app.listen(this.header, 'contextmenu', this._headerContextMenuHandler);
        }
    }
    get autoSortSupported() {
        if (this.dataSource && (this.dataSource.autoSortDisabled !== undefined))
            return !this.dataSource.autoSortDisabled;
        else
            return true;
    }
    _prepareSortColumns(value) {
        // overriden in descendants (e.g. GridView)
    }
    _refreshSortIndicators() {
        // overriden in descendants (e.g. GridView)
    }
    renderGroupHeaderPartial(div, group, offset) {
        // overriden in descendant GroupedTrackList
    }
    get autoSortString() {
        if (this._autoSortString !== undefined)
            return this._autoSortString;
        else
            return this.getDefaultSortString();
    }
    set autoSortString(value) {
        if (this._autoSortString != value) {
            this._autoSortString = value;
            if (this._prepareSortColumns && this._refreshSortIndicators) {
                this._prepareSortColumns(value);
                this._refreshSortIndicators();
            }
            if (this.isSortable /* #19397 */)
                this.forceAutoSort();
        }
    }
    get toolbarActions() {
        if (this._toolbarActions === undefined) {
            if (this.multiselect) {
                this._toolbarActions = [actions.cancelSelection, actions.selectAll];
            }
            else {
                this._toolbarActions = [];
            }
        }
        return this._toolbarActions;
    }
    cancelAutoSort() {
        if (this._lastSorting) {
            cancelPromise(this._lastSorting);
            this._lastSorting = undefined;
        }
    }
    getDefaultSortString() {
        return '';
    }
    getFocusedItemLink() {
        let link;
        if (this.focusedIndex >= 0 && this.dataSource && (this.focusedIndex < this.dataSource.count)) {
            this.dataSource.locked(() => {
                link = this.dataSource.getValueLink(this.focusedIndex);
            });
        }
        return link;
    }
    raiseItemFocusChange() {
        let itmLink = this.getFocusedItemLink();
        if (itmLink) {
            this.raiseEvent('itemfocuschange', {
                link: itmLink
            }, true, false /* don't bubble */);
        }
    }
    raiseItemSelectChange(index) {
        let link;
        if (index >= 0 && this.dataSource && index < this.dataSource.count) {
            this.dataSource.locked(() => {
                link = this.dataSource.getValueLink(index);
            });
        }
        if (link) {
            this.raiseEvent('itemselectchange', {
                link: link
            }, true, false /* don't bubble */);
        }
    }
    getVirtualHeights() {
        let cs = getComputedStyle(this.container, null);
        let totheight = this.viewportSize;
        let headerHeight = parseFloat(cs.getPropertyValue('border-top-width')) + parseFloat(cs.getPropertyValue('padding-top')) + parseFloat(cs.getPropertyValue('margin-top'));
        let footerHeight = parseFloat(cs.getPropertyValue('border-bottom-width')) + parseFloat(cs.getPropertyValue('padding-bottom')) + +parseFloat(cs.getPropertyValue('margin-bottom'));
        if (this.showHeader) {
            headerHeight += getFullHeight(this.header);
        }
        totheight += headerHeight + footerHeight;
        return {
            totalHeight: totheight,
            headerHeight: headerHeight,
            footerHeight: footerHeight
        };
    }
    getFocusedElement() {
        if (this.focusedIndex > -1)
            return this.getDiv(this.focusedIndex);
    }
    updateParentScrollTop() {
        if (this.scrollingParent) {
            if (this.scrollingParent.controlClass && this.scrollingParent.controlClass.getSmoothScrollOffset) {
                this._parentScrollTop = this.scrollingParent.controlClass.getSmoothScrollOffset();
            }
            else {
                this._parentScrollTop = this.scrollingParent.scrollTop;
            }
        }
        else {
            this._parentScrollTop = 0;
        }
    }
    parentScrollFrame(deferDraw) {
        if (this.visible && this.scrollingParent) {
            // Adjust position of the LV header (might need to be attached to the top of the scrolling element)
            this.updateParentScrollTop();
            // JH: The following was removed in order to handle header by 'position: sticky' css. Seems to be working fine, to be tested.
            // var scrollTop = this.scrollingParent.scrollTop; // We need this version of scrollTop for header, not this._parentScrollTop
            // if (scrollTop > this._containerOffsetTop && scrollTop < this._containerOffsetTop + this._containerOffsetHeight)
            //     this.header.style.top = scrollTop - this._containerOffsetTop;
            // else
            //     this.header.style.top = 0;
            this.updateHover();
            if (deferDraw)
                this.deferredDraw();
            else
                this.drawnow();
        }
    }
    selectAll() {
        let handled = false;
        let ds = this.dataSource;
        if (this.multiselect && ds && ds.selectRangeAsync) {
            ds.selectRangeAsync(0, ds.count - 1);
            handled = true;
        }
        return handled;
    }
    cancelSelection() {
        let handled = false;
        let ds = this.dataSource;
        if (ds && ds.clearSelection) {
            ds.clearSelection();
            this.selectionMode = false;
            handled = true;
        }
        return handled;
    }
    setStatus(data) {
        if (this.multiselect) {
            if (!data.selectedCount)
                this.selectionMode = false;
            else if (this.automaticSelectionMode && (data.selectedCount > 1))
                this.selectionMode = true;
        }
        super.setStatus(data);
    }
    // ---------------- Popup handling -----------------------
    getSkip(id, canAdd) {
        for (let i = 0; i < this.skips.length; i++) {
            let skip = this.skips[i];
            if (skip.id === id)
                return skip;
        }
        if (canAdd) {
            // @ts-ignore
            let skip = {
                id: id,
                reservePx: 0
            };
            this.skips.push(skip);
            return skip;
        }
    }
    removeSkip(id) {
        for (let i = 0; i < this.skips.length; i++) {
            let skip = this.skips[i];
            if (skip.id === id) {
                this.skips.splice(i, 1);
                return skip;
            }
        }
    }
    animatePopup(skip, counter) {
        if (skip.animation)
            clearTimeout(skip.animation);
        let startPx = (skip.hide && skip.mix ? skip.oldReservePx : skip.reservePx);
        let endPx = (skip.hide ? (skip.targetPx || 0) : this.getPopupHeight(this.popupDiv));
        skip.targetPx = endPx;
        let startOpacity = (skip.opacity || 0);
        let animstart = performance.now();
        let animTime = (skip.animate ? 1000 * animTools.animationTime : 0);
        if (fullLVDebug)
            ODS('***Animate: ' + startPx + ' -> ' + endPx);
        let myanimation = () => {
            if (this._cleanUpCalled || skip.cancelAnimation)
                return;
            let duration = performance.now() - animstart;
            let oldPx = skip.reservePx;
            let newPx = startPx;
            if (duration >= animTime || counter != this.popupCounter) {
                // End animation
                newPx = endPx;
                skip.opacity = 1;
                skip.div.style.opacity = 1;
                skip.targetPx = undefined;
                if (!skip.hide && this.popupIndicator)
                    this.popupIndicator.style.opacity = 1;
                if (skip.hide)
                    this.cleanPopup(skip);
            }
            else {
                // Animation step
                let progress = animTools.easingFn[animTools.defaultEasing](duration / animTime);
                newPx = startPx + Math.round((endPx - startPx) * progress);
                skip.opacity = startOpacity + (1 - startOpacity) * Math.min(1, Math.pow(duration / (animTime * 0.5 /*faster blending looks better*/), 0.33));
                if (!skip.hide) {
                    if (this.popupIndicator)
                        this.popupIndicator.style.opacity = skip.opacity;
                    if (skip.mix)
                        skip.div.style.opacity = skip.opacity;
                }
                skip.animation = this.requestTimeout(myanimation, 15); // TODO: Better mix with our usage of rAF()?
            }
            if (!skip.hide || !skip.mix) {
                skip.reservePx = newPx;
                if (skip.adjustScroll) {
                    this.adjustScroll(skip.reservePx - oldPx);
                }
            }
            skip.div.style.height = newPx;
            notifyLayoutChangeDown(skip.div);
            this._adjustSizeNeeded = true;
            this.deferredDraw();
        };
        myanimation();
    }
    updatePopupRequest(div, defer) {
        let _this = this;
        this.requestTimeout(function () {
            _this.updatePopup(div.counter);
        }, defer ? 25 : 0, 'updatePopup', false /* prefer last request */);
    }
    getPopupHeight(popupDiv) {
        return popupDiv.targetOffsetHeight ? popupDiv.targetOffsetHeight : popupDiv.offsetHeight;
    }
    updatePopup(counter) {
        if (counter != this.popupCounter)
            return; // An old request, ignore
        let skip = this.getSkip('popup');
        if (!skip)
            return;
        if (!skip.hide) {
            if (skip.targetPx === this.getPopupHeight(this.popupDiv))
                return; // Ignore update in case we already animate to the same dimensions
            skip.rendered = true;
            let top = this.getItemTopOffset(skip.afterIndex);
            let oldskip = this.getSkip('oldpopup');
            let aboveShift = 0;
            if (oldskip) {
                let oldtop = this.getItemTopOffset(oldskip.afterIndex);
                if (oldtop < top) {
                    oldskip.adjustScroll = true; // Move scroll together with hiding this popup
                    //                aboveShift = -oldskip.reservePx; // JH: This was wrong, it seems that we don't need 'aboveShift' at all?
                } // else 
                // JH: TODO: Fix animation when a popup near end of a list is shown (isn't placed correctly now)
                //                if (oldtop > top) {
                //                    aboveShift = Math.max(0, oldskip.reservePx - (this.viewportSize - this.getScrollBottom()));
                //                    if (aboveShift>0)
                //                        oldskip.adjustScroll = true; // Move scroll together with hiding this popup
                //                }
            }
            this.scrollToView(top, top + this.rowDimension + this.getPopupHeight(this.popupDiv), aboveShift);
            if (oldskip) {
                if (oldskip.mix)
                    oldskip.targetPx = this.popupDiv.offsetHeight;
                this.animatePopup(oldskip, this.popupCounter);
            }
            skip.shown = true;
        }
        notifyLayoutChangeDown(this.popupDiv); // Make sure it's properly rendered
        this.animatePopup(skip, this.popupCounter);
    }
    cleanPopup(skip) {
        if (skip) {
            this.removeSkip(skip.id);
            if (!skip.cloned)
                this.cancelOldPopup();
            this.popupCache.push(skip.div);
            skip.div.style.top = '-999999px'; // To hide it
            if (skip.id === 'popup')
                this.popupDiv = undefined;
        }
    }
    isPopupShown() {
        let skip = this.getSkip('popup');
        return (skip !== undefined) && !skip.hide;
    }
    closePopup() {
        let skip = this.getSkip('popup');
        if (skip) {
            this.showPopup(skip.afterIndex); // Close already shown pop-up
        }
    }
    cancelOldPopup() {
        if (this.popupDiv) {
            if (this.popupDiv.controlClass)
                this.popupDiv.controlClass.cleanUpPromises();
        }
    }
    showPopup(index) {
        if (!this.dataSource) // TODO: needed?
            return;
        let _this = this;
        let skip = this.getSkip('popup', true);
        skip.hide = false;
        skip.mix = false;
        skip.animate = true;
        if (skip.div) {
            if (skip.afterIndex == index) {
                // Hide this already shown item
                skip.hide = true;
                this.updatePopup(this.popupCounter);
                return;
            }
            else {
                let topold = this.getItemTopOffset(skip.afterIndex);
                let topnew = this.getItemTopOffset(index);
                // Remove any old animation of a hiding popup
                let oldskip = this.getSkip('oldpopup');
                let wasold = false;
                if (oldskip) {
                    this.cleanPopup(oldskip);
                    oldskip.cancelAnimation = true;
                    wasold = true;
                }
                // Animate hiding of the old item and create a new one            
                oldskip = skip;
                oldskip.cloned = true;
                oldskip.id = 'oldpopup';
                oldskip.hide = true;
                oldskip.animate = !wasold;
                oldskip.div.style.zIndex = 99; // Behind the newly showing pop-up
                this.cancelOldPopup(); // To create a new one below
                skip = this.getSkip('popup', true);
                skip.hide = false;
                skip.mix = false;
                skip.animate = true;
                if (topold == topnew) { // Just animate the transition from one pop-up to another
                    skip.reservePx = oldskip.reservePx;
                    skip.mix = true;
                    skip.animate = !wasold;
                    oldskip.oldReservePx = oldskip.reservePx;
                    oldskip.reservePx = 0;
                    oldskip.mix = true;
                }
            }
        }
        if (!skip.div) {
            // eslint-disable-next-line no-cond-assign
            if (skip.div = this.popupCache.pop()) {
                this.popupDiv = skip.div.firstChild;
            }
            else {
                skip.div = document.createElement('div');
                skip.div.style.overflow = 'hidden';
                skip.div.style.position = 'absolute';
                skip.div.className = 'lvPopupContainer';
                skip.div.controlClass = new Control(skip.div);
                this.addItemToCanvas(skip.div);
                this.popupDiv = document.createElement('div');
                this.popupDiv.parentListView = this;
                this.popupDiv.className = 'lvPopup';
                this.popupDiv.style.position = 'absolute';
                this.popupDiv.style.left = '0';
                this.popupDiv.style.top = '0';
                this.popupDiv.style.right = '0';
                skip.div.appendChild(this.popupDiv);
                let popupCloseBtn = document.createElement('div');
                popupCloseBtn.className = 'hoverHeader closeButton';
                popupCloseBtn.setAttribute('data-tip', _('Close popup'));
                loadIconFast('close', function (icon) {
                    if (popupCloseBtn && this.popupDiv && !window._cleanupCalled) // not cleared yet
                        setIconFast(popupCloseBtn, icon);
                    setIconAriaLabel(popupCloseBtn, _('Close popup'));
                }.bind(this));
                skip.div.controlClass.localListen(popupCloseBtn, 'click', function (e) {
                    this.closePopup();
                    e.stopPropagation();
                }.bind(this));
                skip.div.appendChild(popupCloseBtn);
            }
            skip.div.style.height = '0px'; // Initial size
            skip.div.style.zIndex = 100;
            skip.div.style.width = (this.getVisibleColsDim() - this.colGroupDimension) + 'px';
        }
        let currItem;
        this._dataSource.locked(function () {
            currItem = _this.dataSource.getValue(index); // do not use fast object, so popup can hold reference to this item
            if (currItem)
                _this.popupDiv.itemID = currItem.persistentID;
        });
        if (currItem) {
            skip.afterIndex = index;
            this.popupDiv.itemIndex = index;
            this.popupCounter = (this.popupCounter + 1) || 0;
            this.popupDiv.counter = this.popupCounter;
            if (this.renderPopup(this.popupDiv, currItem))
                this.updatePopup(this.popupCounter);
            else {
                // Async update of pop-up dimensions
                this.updatePopupRequest(this.popupDiv, true /*defer*/);
            }
        }
    }
    popupDataSource() {
        if (this.isPopupShown() && this.popupDiv && this.popupDiv.controlClass) { // @ts-ignore            
            if (this.popupDiv.controlClass.getMergedTracklist) // @ts-ignore
                return this.popupDiv.controlClass.getMergedTracklist(); // @ts-ignore
            if (this.popupDiv.controlClass._getTracklist) // @ts-ignore
                return this.popupDiv.controlClass._getTracklist();
        }
        return null;
    }
    reloadSettings() {
        let sett = settings.get('Appearance,Options');
        this.smoothScroll = sett.Appearance.SmoothScroll;
        this.gridPopupDelay = sett.Options.GridPopupDelay;
    }
    moveFocusRight( /*editable?: boolean*/) {
        if (this.itemCount > 1) {
            let newFocus;
            if (this.focusedIndex < 0)
                newFocus = 0;
            else
                newFocus = Math.min(this.focusedIndex + 1, this.itemCount - 1);
            this.focusedIndex = newFocus;
            return true;
        }
        else
            return false;
    }
    moveFocusLeft( /*editable?: boolean*/) {
        if (this.itemCount > 1) {
            let newFocus;
            if (this.focusedIndex < 0)
                newFocus = 0;
            else
                newFocus = Math.max(this.focusedIndex - 1, 0);
            this.focusedIndex = newFocus;
            return true;
        }
        else
            return false;
    }
    // Draw pop-up interior
    renderPopup(div, item) {
        return false; // overriden in descendants
    }
    get scrollingParent() {
        // LS: note that scrollingParent can be changed when control is re-used from controlCache and gets another scroll parent
        //     keep in mind that scrollingParent doesn't always have controlClass, it can be any DIV with 'scrollable' class (or a Scroller component with controlClass)
        if (!this._scrollingParent || !isChildOf(this._scrollingParent, this.container)) {
            this._scrollingParent = undefined;
            let ctrl = this.container;
            while ((ctrl = ctrl.parentNode) && (ctrl instanceof Element)) { // We need DOM hierarchy, not offsetParent
                let style = getComputedStyle(ctrl);
                if ((ctrl.classList.contains('listview')) || (ctrl.classList.contains('dynroot')) ||
                    style.overflowX === 'auto' || style.overflowX === 'scroll' || style.overflowY === 'auto' || style.overflowY === 'scroll') {
                    // JH: For some reason the condition above is fullfilled even if we set all divs to overflow: hidden. They are still calculated as 'auto', not sure why.
                    if (ctrl.classList.contains('lvCanvas'))
                        continue; // Ignore scrolling canvas of a listview - use the listview itself
                    this._scrollingParent = ctrl;
                    this.header.style.zIndex = 10000; // So that scrolling header can be kept before other elements
                    // Listen to scroll event and make sure we are properly unlistened later                        
                    this.localListen(ctrl, 'scroll', function (e) {
                        // LV needs to redraw in case its position is changed (new content might be visible)
                        this.parentScrollFrame(true);
                    }.bind(this));
                    break;
                }
            }
            if (!this._scrollingParent) {
                if (this.dynamicSize)
                    this.container.classList.remove('showInline');
                this._scrollingParent = this.container.offsetParent; // Our direct parent will work for our purposes.
            }
            else {
                if (this.dynamicSize)
                    this.container.classList.add('showInline');
            }
        }
        return this._scrollingParent;
    }
    get oneRow() {
        return this._oneRow;
    }
    set oneRow(value) {
        if (value) {
            this.showRowCount = 1;
        }
        else {
            this.showRowCount = 0;
        }
        if (this._oneRow != value) {
            this.oldWidth = -1;
            this.oldHeight = -1;
            this.adjustSize(false);
            this.invalidateAll();
        }
        this._oneRow = value;
    }
    get dynamicSize() {
        return this._dynamicSize;
    }
    set dynamicSize(value) {
        if (value) {
            this.fill.classList.remove('fill');
            this.canvas.style.height = '';
            this.canvas.style.width = '';
        }
        else {
            this.fill.classList.add('fill');
            this.canvas.style.height = '100%';
            this.canvas.style.width = '100%';
        }
        this._dynamicSize = value;
    }
    get multiselect() {
        return this._multiselect;
    }
    set multiselect(value) {
        if (this._multiselect === value)
            return;
        this._multiselect = value;
        // have to regenerate divs and recompile binding, it could be dependent on multiselect value, #14522
        this.clearDivs();
        this.bindFn = undefined;
        this.invalidateNeeded = true;
    }
    get enableIncrementalSearch() {
        if (this._incrementalSearchEnabled != null) {
            return this._incrementalSearchEnabled;
        }
        else {
            // wasn't enabled/disabled for this component, so take the value from settings
            let state = app.getValue('search_settings', {
                contextualSearchMode: 0
            });
            return (state.contextualSearchMode == 1);
        }
    }
    set enableIncrementalSearch(value) {
        this._incrementalSearchEnabled = value;
    }
}
registerClass(ListView);
listview.js (220,481 bytes)   

rusty

2023-10-04 15:53

administrator   ~0072998

It took a bit longer to replicate, but it still occurrs.
Crashlog: 02B1704E

Note: in the debug log, the endless db activity started at around line 77000.

Ludek

2023-10-04 17:37

developer   ~0073000

Fixed in 2818

peke

2023-10-14 20:55

developer   ~0073116

Verified 2819

Unable to replicate after several hours of smoke testing.