View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0020274 | MMW 5 | Tracklist | public | 2023-10-04 02:34 | 2023-10-16 17:07 |
| Reporter | rusty | Assigned To | |||
| Priority | urgent | Severity | crash | Reproducibility | sometimes |
| Status | closed | Resolution | fixed | ||
| Product Version | 2024.0 | ||||
| Target Version | 2024.0 | Fixed in Version | 2024.0 | ||
| Summary | 0020274: MM crashes on switching between Music views (44A462FE) | ||||
| Description | Switched 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). | ||||
| Tags | No tags attached. | ||||
| Fixed in build | 2818 | ||||
|
|
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);
|
|
|
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. |
|
|
Fixed in 2818 |
|
|
Verified 2819 Unable to replicate after several hours of smoke testing. |