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 | 5.1 | ||||
Target Version | 5.1 | Fixed in Version | 5.1 | ||
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. |