@syncfusion/ej2-filemanager
Version:
Essential JS 2 FileManager Component
325 lines (311 loc) • 16.9 kB
text/typescript
import { EventHandler, isNullOrUndefined, detach, formatUnit } from '@syncfusion/ej2-base';
import { FileManager } from '../base';
import { ListBase } from '@syncfusion/ej2-lists';
import { createElement, selectAll } from '@syncfusion/ej2-base';
import * as CLS from '../base/classes';
import { LargeIconsView } from '../layout';
export class Virtualization {
constructor(instance: FileManager) {
this.filemanagerInstance = instance;
this.largeIconInstance = instance.largeiconsviewModule;
}
// The file manager component instance
private filemanagerInstance: FileManager;
// The LargeIconsView module
private largeIconInstance: LargeIconsView;
// The number of items that can be rendered in the viewport
private itemCount: number;
// The number of items that can be rendered per row
private rowItemCount: number;
// The rendered items
private items: Object[];
// The rendered items element
private itemList: HTMLElement[];
// The current scroll position of the view
private scrollPosition: number;
// The total height for rendering all items
private totalHeight: number;
// The height of each list item
private listItemHeight: number;
// The height of the top virtual element
private topElementHeight: number;
// The height of the bottom virtual element
private bottomElementHeight: number;
// The number of rendered items
private renderedCount: number;
// The last row's items
private lastRowCount: number;
// The top div element
private topElement: HTMLElement;
// The bottom div element
private bottomElement: HTMLElement;
// The number of rows scrolled
private listDiff: number;
/**
* Sets up UI virtualization for the large icon view.
*
* @returns {void}
*/
public setUIVirtualization(): void {
// Get the current view data source
const currentViewItems: { [key: string]: object; }[] = this.largeIconInstance.items as { [key: string]: object; }[];
// Get the first item in the data source
const firstItem: { [key: string]: object }[] = currentViewItems.slice(0, 1);
// Create a list element from the first item in the data source
const listElements: HTMLElement = ListBase.createListFromJson(createElement, firstItem, this.largeIconInstance.listObj);
// Get the list items from the list element
this.itemList = Array.prototype.slice.call(selectAll('.' + CLS.LIST_ITEM, listElements));
// Append the list element to the large icon element
this.largeIconInstance.element.appendChild(listElements);
if (this.itemList.length !== 0 && this.largeIconInstance.element.querySelector('.' + CLS.EMPTY)) {
this.largeIconInstance.element.removeChild(this.largeIconInstance.element.querySelector('.' + CLS.EMPTY));
}
// Get the total number of items
this.itemCount = this.getItemCount(Object.keys(this.largeIconInstance.allItems).length);
// Remove the first child element from the large icon element
this.largeIconInstance.element.firstChild.remove();
// Set the items for the large icon view to the current view data source, limited to the number of items to display
this.largeIconInstance.items = currentViewItems.slice(0, this.itemCount);
}
/**
* Sets the height of the top and bottom elements that are used for virtualization.
* These elements are used to give the appearance of an infinitely scrolling list.
*
* @returns {void}
*/
public setUlElementHeight(): void {
// Calculate the number of items in the last row
this.lastRowCount = (this.largeIconInstance.allItems.length - this.itemCount) % this.rowItemCount ?
(this.largeIconInstance.allItems.length - this.itemCount) % this.rowItemCount : this.rowItemCount;
// Create top and bottom elements
this.topElement = this.filemanagerInstance.createElement('div');
this.topElement.classList.add('e-virtual-top');
this.largeIconInstance.element.firstElementChild.insertBefore(
this.topElement, this.largeIconInstance.element.firstElementChild.firstChild);
this.bottomElement = this.filemanagerInstance.createElement('div');
this.bottomElement.classList.add('e-virtual-bottom');
this.largeIconInstance.element.firstElementChild.insertBefore(this.bottomElement, null);
// Get the margin value for list items
const marginValue: number = parseInt(window.getComputedStyle(this.largeIconInstance.itemList[0]).getPropertyValue('margin-top'), 10) +
parseInt(window.getComputedStyle(this.largeIconInstance.itemList[0]).getPropertyValue('margin-bottom'), 10);
// Calculate the height of a single list item
this.listItemHeight = this.largeIconInstance.itemList[0].getBoundingClientRect().height + marginValue;
// Calculate the total height of the list
this.totalHeight = (Object.keys(this.largeIconInstance.allItems).length / this.rowItemCount) * this.listItemHeight;
// Set the initial height of the top and bottom elements
this.topElement.style.height = 0 + 'px';
this.bottomElement.style.height = this.totalHeight + 'px';
// Initialize the top and bottom element heights
this.topElementHeight = 0;
this.bottomElementHeight = this.totalHeight;
// Initialize the list difference variable
this.listDiff = 0;
// Set the initial rendered count
this.renderedCount = this.itemCount;
}
/**
* Calculates the number of items to display in the list based on the available width and height.
*
* @param {number} dataSourceLength The length of the data source.
* @returns {number} The number of items to display.
*/
private getItemCount(dataSourceLength: number): number {
// Get the margin values for list items
const widthMargin: number = parseInt(window.getComputedStyle(this.itemList[0]).getPropertyValue('margin-right'), 10) +
parseInt(window.getComputedStyle(this.itemList[0]).getPropertyValue('margin-left'), 10);
// Calculate the number of items that can fit in a single row
this.rowItemCount =
Math.floor(parseFloat(formatUnit(this.largeIconInstance.element.firstElementChild.clientWidth)) /
(this.itemList[0].offsetWidth + widthMargin));
// Calculate the number of items that can fit in the available height
let itemCount: number =
this.rowItemCount *
(Math.round(parseFloat(formatUnit(this.largeIconInstance.element.clientHeight)) / this.itemList[0].offsetHeight));
// If the calculated item count is greater than the data source length, set the item count to the data source length
if (itemCount > dataSourceLength || itemCount === 0) {
itemCount = dataSourceLength;
}
return itemCount;
}
/**
* Wires or un wires the scroll event for the list element.
*
* @param {boolean} destroy - Set `true` to unwire the scroll event.
* @returns {void}
*/
public wireScrollEvent(destroy: boolean): void {
if (!destroy) {
// Wire the scroll event
EventHandler.add(this.largeIconInstance.element.firstElementChild, 'scroll', this.onVirtualUiScroll, this);
} else {
// Unwire the scroll event
EventHandler.remove(this.largeIconInstance.element.firstElementChild, 'scroll', this.onVirtualUiScroll);
}
}
/**
* Handles the scroll event for the list element.
* This method updates the top and bottom elements and the displayed items based on the scroll position.
*
* @returns {void}
* @private
*/
private onVirtualUiScroll(): void {
// Set the starting height to 0
const startingHeight: number = 0;
// Get the current scroll position
this.scrollPosition = isNullOrUndefined(this.scrollPosition) ? 0 : this.scrollPosition;
const scroll: number = this.getscrollerHeight(startingHeight);
// Calculate the height of the top element
this.topElementHeight = this.listItemHeight * Math.floor(scroll / this.listItemHeight);
// Calculate the height of the bottom element
this.bottomElementHeight = this.totalHeight - this.topElementHeight;
// If the scroll position is less than or equal to the total height, set the top and bottom element heights.
// Otherwise, set the top element height to the total height and the bottom element height to 0.
[this.topElementHeight, this.bottomElementHeight] = scroll <= this.totalHeight ?
[this.topElementHeight, this.bottomElementHeight] : [this.totalHeight, 0];
// If the top element height has changed, update the top and bottom element heights and re-render the items.
if (this.topElementHeight !== parseFloat(this.topElement.style.height)) {
this.topElement.style.height = this.topElementHeight + 'px';
this.bottomElement.style.height = this.bottomElementHeight + 'px';
// Check whether the scroll direction is upward or downward
if (scroll > this.scrollPosition) {
// Scrolling is upward
const listDiff: number = Math.round(((this.topElementHeight / this.listItemHeight) - this.listDiff));
this.onNormalScroll(listDiff, true);
} else {
// Scrolling is downward
const listDiff: number = Math.round((this.listDiff - (this.topElementHeight / this.listItemHeight)));
this.onNormalScroll(listDiff, false);
}
}
// Update the list difference and scroll position variables
this.listDiff = Math.round(this.topElementHeight / this.listItemHeight);
this.scrollPosition = scroll;
// Update the list of items and the items property of the largeIconInstance
this.largeIconInstance.itemList = Array.prototype.slice.call(selectAll('.' + CLS.LIST_ITEM, this.largeIconInstance.element));
this.itemCount = this.itemCount !== this.largeIconInstance.itemList.length ?
this.largeIconInstance.itemList.length : this.itemCount;
this.largeIconInstance.items = this.largeIconInstance.allItems.slice(this.renderedCount -
this.itemCount, this.renderedCount);
}
/**
* Calculates the current scroll position of the list element.
*
* @param {number} startingHeight The starting height from which to calculate the scroll position.
* @returns {number} The current scroll position.
* @private
*/
private getscrollerHeight(startingHeight: number): number {
// If the scroll position is less than or equal to the starting height, return 0.
// Otherwise, return the scroll position minus the starting height.
return ((this.largeIconInstance.element.firstElementChild.scrollTop - startingHeight) <= 0) ? 0 :
(this.largeIconInstance.element.firstElementChild.scrollTop - startingHeight);
}
/**
* This method updates the displayed items and the selection based on the scroll direction.
*
* @param {number} listDiff The number of rows to update.
* @param {boolean} isScrollingDown If set to true, the scroll direction is downward.
* @returns {void}
* @private
*/
private onNormalScroll(listDiff: number, isScrollingDown: boolean): void {
// Update the displayed items
for (let i: number = 0; i < listDiff; i++) {
this.updateUI(isScrollingDown);
}
}
/**
* Updates the items in the large icons view.
*
* @param {boolean} isScrollingDown - If set to true, the scroll direction is downward.
* @returns {void}
* @private
*/
private updateUI(isScrollingDown: boolean): void {
if (isScrollingDown) {
// Get the next batch of items to be displayed
this.items = this.largeIconInstance.allItems.slice(this.renderedCount, this.renderedCount + this.rowItemCount);
// If there are items to be displayed, create list elements for them and append them to the list
if (this.items.length > 0) {
const listElements: HTMLElement = ListBase.createListFromJson(
createElement, <{ [key: string]: Object; }[]>this.items, this.largeIconInstance.listObj
);
this.itemList = Array.prototype.slice.call(selectAll('.' + CLS.LIST_ITEM, listElements));
this.itemList.forEach((liEle: HTMLElement) => {
this.largeIconInstance.element.firstElementChild.insertBefore(liEle, this.bottomElement);
});
// Update the rendered count variable
this.renderedCount = (this.largeIconInstance.allItems.length >= this.renderedCount + this.rowItemCount) ?
this.renderedCount + this.rowItemCount : this.renderedCount + this.lastRowCount;
// Remove the first batch of items from the list
for (let i: number = 0; i < this.rowItemCount; i++) {
detach(this.topElement.nextElementSibling);
}
}
} else {
// Scrolling up
let lastItemIndex: number;
let isAllRendered: boolean;
if (this.renderedCount === this.largeIconInstance.allItems.length) {
// Set lastItemIndex to the last item in the last row
lastItemIndex = this.renderedCount - (this.itemCount - this.rowItemCount + this.lastRowCount);
// Set renderedCount to the total number of items that have been rendered, except for the items in the last row
this.renderedCount = ((this.renderedCount - this.lastRowCount) < this.itemCount) ?
this.itemCount : (this.renderedCount - this.lastRowCount);
// Set isAllRendered to true to indicate that all items have been rendered
isAllRendered = true;
} else {
// Set lastItemIndex to the last item in the current row
lastItemIndex = this.renderedCount - this.itemCount;
// Set renderedCount to the total number of items that have been rendered, except for the items in the current row
this.renderedCount = ((this.renderedCount - this.rowItemCount) < this.itemCount) ?
this.itemCount : (this.renderedCount - this.rowItemCount);
}
// Set startItemIndex to the first item in the current or previous row
const startItemIndex: number = (lastItemIndex - this.rowItemCount > 0) ? lastItemIndex - this.rowItemCount : 0;
// Set the items array to the items in the current or previous row
this.items = this.largeIconInstance.allItems.slice(startItemIndex, lastItemIndex);
if (this.items.length > 0) {
// Create a list of elements from the items array
const listElements: HTMLElement = ListBase.createListFromJson(
createElement, <{ [key: string]: Object; }[]>this.items, this.largeIconInstance.listObj
);
// Set the itemList array to the list items in the list elements
this.itemList = Array.prototype.slice.call(selectAll('.' + CLS.LIST_ITEM, listElements));
// Add the items to the beginning of the list
for (let len: number = this.itemList.length; len > 0; len--) {
this.largeIconInstance.element.firstElementChild.insertBefore(
this.itemList[len - 1], this.topElement.nextElementSibling
);
}
// Remove the last row of items from the list
for (let i: number = 0; i < ((isAllRendered) ? this.lastRowCount : this.rowItemCount); i++) {
detach(this.bottomElement.previousElementSibling);
}
}
}
}
/**
* For internal use only - Get the module name.
*
* @returns {string} - returns the module name.
* @private
*/
private getModuleName(): string {
return 'virtualization';
}
/**
* Destroys the component.
*
* @returns {void}
*/
public destroy(): void {
// If the file manager has already been destroyed, return immediately
if (this.filemanagerInstance.isDestroyed) { return; }
// If the large icon element has a child element, unwire the scroll event
if (!isNullOrUndefined(this.largeIconInstance.element.firstElementChild)) {
this.wireScrollEvent(true);
}
}
}