@mhmdaljefri/revogrid
Version:
Virtual reactive data grid component - RevoGrid.
400 lines (399 loc) • 12.5 kB
JavaScript
import { Component, Event, h, Method, Element, Prop, Host } from '@stencil/core';
import each from 'lodash/each';
import GridResizeService from '../revo-grid/viewport.resize.service';
import LocalScrollService from '../../services/localScrollService';
import { CONTENT_SLOT, FOOTER_SLOT, HEADER_SLOT } from '../revo-grid/viewport.helpers';
/**
* Service for tracking grid scrolling
*/
export class RevogrViewportScroll {
constructor() {
this.scrollThrottling = 30;
/**
* Width of inner content
*/
this.contentWidth = 0;
/**
* Height of inner content
*/
this.contentHeight = 0;
this.oldValY = this.contentHeight;
this.oldValX = this.contentWidth;
/**
* Last mw event time for trigger scroll function below
* If mousewheel function was ignored we still need to trigger render
*/
this.mouseWheelScroll = { rgCol: 0, rgRow: 0 };
}
async setScroll(e) {
var _a;
this.latestScrollUpdate(e.dimension);
(_a = this.scrollService) === null || _a === void 0 ? void 0 : _a.setScroll(e);
}
/**
* update on delta in case we don't know existing position or external change
* @param e
*/
async changeScroll(e) {
if (e.delta) {
switch (e.dimension) {
case 'rgCol':
e.coordinate = this.horizontalScroll.scrollLeft + e.delta;
break;
case 'rgRow':
e.coordinate = this.verticalScroll.scrollTop + e.delta;
break;
}
this.setScroll(e);
}
return e;
}
connectedCallback() {
/**
* Bind scroll functions for farther usage
*/
if ('ontouchstart' in document.documentElement) {
this.scrollThrottling = 0;
}
else {
this.verticalMouseWheel = this.onVerticalMouseWheel.bind(this, 'rgRow', 'deltaY');
this.horizontalMouseWheel = this.onHorizontalMouseWheel.bind(this, 'rgCol', 'deltaX');
}
/**
* Create local scroll service
*/
this.scrollService = new LocalScrollService({
beforeScroll: e => this.scrollViewport.emit(e),
afterScroll: e => {
switch (e.dimension) {
case 'rgCol':
this.horizontalScroll.scrollLeft = e.coordinate;
break;
case 'rgRow':
this.verticalScroll.scrollTop = e.coordinate;
break;
}
},
});
}
componentDidLoad() {
/**
* Track horizontal viewport resize
*/
this.horisontalResize = new GridResizeService(this.horizontalScroll, {
resize: entries => {
var _a, _b;
let height = ((_a = entries[0]) === null || _a === void 0 ? void 0 : _a.contentRect.height) || 0;
if (height) {
height -= this.header.clientHeight + this.footer.clientHeight;
}
const els = {
rgRow: {
size: height,
contentSize: this.contentHeight,
scroll: this.verticalScroll.scrollTop,
},
rgCol: {
size: ((_b = entries[0]) === null || _b === void 0 ? void 0 : _b.contentRect.width) || 0,
contentSize: this.contentWidth,
scroll: this.horizontalScroll.scrollLeft,
},
};
each(els, (item, dimension) => {
var _a;
this.resizeViewport.emit({ dimension, size: item.size });
(_a = this.scrollService) === null || _a === void 0 ? void 0 : _a.scroll(item.scroll, dimension, true);
// track scroll visibility on outer element change
this.setScrollVisibility(dimension, item.size, item.contentSize);
});
},
});
}
/**
* Check if scroll present or not per type
* Trigger this method on inner content size change or on outer element size change
* If inner content bigger then outer size then scroll is present and mousewheel binding required
* @param type - dimension type 'rgRow/y' or 'rgCol/x'
* @param size - outer content size
* @param innerContentSize - inner content size
*/
setScrollVisibility(type, size, innerContentSize) {
// test if scroll present
const hasScroll = size < innerContentSize;
let el;
// event reference for binding
let event;
switch (type) {
case 'rgCol':
el = this.horizontalScroll;
event = this.horizontalMouseWheel;
break;
case 'rgRow':
el = this.verticalScroll;
event = this.verticalMouseWheel;
break;
}
// based on scroll visibility assign or remove class and event
if (hasScroll) {
el.classList.add(`scroll-${type}`);
el.addEventListener('mousewheel', event);
}
else {
el.classList.remove(`scroll-${type}`);
el.removeEventListener('mousewheel', event);
}
this.scrollchange.emit({ type, hasScroll });
}
disconnectedCallback() {
this.verticalScroll.removeEventListener('mousewheel', this.verticalMouseWheel);
this.horizontalScroll.removeEventListener('mousewheel', this.horizontalMouseWheel);
this.horisontalResize.destroy();
}
async componentDidRender() {
// scroll update if number of rows changed
if (this.contentHeight < this.oldValY && this.verticalScroll) {
this.verticalScroll.scrollTop += this.contentHeight - this.oldValY;
}
this.oldValY = this.contentHeight;
// scroll update if number of cols changed
if (this.contentWidth < this.oldValX) {
this.horizontalScroll.scrollLeft += this.contentWidth - this.oldValX;
}
this.oldValX = this.contentWidth;
this.scrollService.setParams({
contentSize: this.contentHeight,
clientSize: this.verticalScroll.clientHeight,
virtualSize: 0,
}, 'rgRow');
this.scrollService.setParams({
contentSize: this.contentWidth,
clientSize: this.horizontalScroll.clientWidth,
virtualSize: 0,
}, 'rgCol');
this.setScrollVisibility('rgRow', this.verticalScroll.clientHeight, this.contentHeight);
this.setScrollVisibility('rgCol', this.horizontalScroll.clientWidth, this.contentWidth);
}
render() {
return (h(Host, { onScroll: (e) => this.onScroll('rgCol', e) },
h("div", { class: "inner-content-table", style: { width: `${this.contentWidth}px` } },
h("div", { class: "header-wrapper", ref: e => (this.header = e) },
h("slot", { name: HEADER_SLOT })),
h("div", { class: "vertical-inner", ref: el => (this.verticalScroll = el), onScroll: (e) => this.onScroll('rgRow', e) },
h("div", { class: "content-wrapper", style: { height: `${this.contentHeight}px` } },
h("slot", { name: CONTENT_SLOT }))),
h("div", { class: "footer-wrapper", ref: e => (this.footer = e) },
h("slot", { name: FOOTER_SLOT })))));
}
/**
* Extra layer for scroll event monitoring, where MouseWheel event is not passing
* We need to trigger scroll event in case there is no mousewheel event
*/
onScroll(dimension, e) {
var _a;
const target = e.target;
let scroll = 0;
switch (dimension) {
case 'rgCol':
scroll = target === null || target === void 0 ? void 0 : target.scrollLeft;
break;
case 'rgRow':
scroll = target === null || target === void 0 ? void 0 : target.scrollTop;
break;
}
const change = new Date().getTime() - this.mouseWheelScroll[dimension];
if (change > this.scrollThrottling) {
(_a = this.scrollService) === null || _a === void 0 ? void 0 : _a.scroll(scroll, dimension);
}
}
/** remember last mw event time */
latestScrollUpdate(dimension) {
this.mouseWheelScroll[dimension] = new Date().getTime();
}
/**
* On vertical mousewheel event
* @param type
* @param delta
* @param e
*/
onVerticalMouseWheel(type, delta, e) {
var _a;
e.preventDefault();
const pos = this.verticalScroll.scrollTop + e[delta];
(_a = this.scrollService) === null || _a === void 0 ? void 0 : _a.scroll(pos, type, undefined, e[delta]);
this.latestScrollUpdate(type);
}
/**
* On horizontal mousewheel event
* @param type
* @param delta
* @param e
*/
onHorizontalMouseWheel(type, delta, e) {
var _a;
e.preventDefault();
const pos = this.horizontalScroll.scrollLeft + e[delta];
(_a = this.scrollService) === null || _a === void 0 ? void 0 : _a.scroll(pos, type, undefined, e[delta]);
this.latestScrollUpdate(type);
}
static get is() { return "revogr-viewport-scroll"; }
static get originalStyleUrls() { return {
"$": ["revogr-viewport-scroll-style.scss"]
}; }
static get styleUrls() { return {
"$": ["revogr-viewport-scroll-style.css"]
}; }
static get properties() { return {
"contentWidth": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Width of inner content"
},
"attribute": "content-width",
"reflect": false,
"defaultValue": "0"
},
"contentHeight": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Height of inner content"
},
"attribute": "content-height",
"reflect": false,
"defaultValue": "0"
}
}; }
static get events() { return [{
"method": "scrollViewport",
"name": "scrollViewport",
"bubbles": false,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": ""
},
"complexType": {
"original": "RevoGrid.ViewPortScrollEvent",
"resolved": "{ dimension: DimensionType; coordinate: number; delta?: number; }",
"references": {
"RevoGrid": {
"location": "import",
"path": "../../interfaces"
}
}
}
}, {
"method": "resizeViewport",
"name": "resizeViewport",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": ""
},
"complexType": {
"original": "RevoGrid.ViewPortResizeEvent",
"resolved": "{ dimension: DimensionType; size: number; }",
"references": {
"RevoGrid": {
"location": "import",
"path": "../../interfaces"
}
}
}
}, {
"method": "scrollchange",
"name": "scrollchange",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": ""
},
"complexType": {
"original": "{ type: RevoGrid.DimensionType; hasScroll: boolean; }",
"resolved": "{ type: DimensionType; hasScroll: boolean; }",
"references": {
"RevoGrid": {
"location": "import",
"path": "../../interfaces"
}
}
}
}]; }
static get methods() { return {
"setScroll": {
"complexType": {
"signature": "(e: RevoGrid.ViewPortScrollEvent) => Promise<void>",
"parameters": [{
"tags": [],
"text": ""
}],
"references": {
"Promise": {
"location": "global"
},
"RevoGrid": {
"location": "import",
"path": "../../interfaces"
}
},
"return": "Promise<void>"
},
"docs": {
"text": "",
"tags": []
}
},
"changeScroll": {
"complexType": {
"signature": "(e: RevoGrid.ViewPortScrollEvent) => Promise<RevoGrid.ViewPortScrollEvent>",
"parameters": [{
"tags": [{
"text": "e",
"name": "param"
}],
"text": ""
}],
"references": {
"Promise": {
"location": "global"
},
"RevoGrid": {
"location": "import",
"path": "../../interfaces"
}
},
"return": "Promise<ViewPortScrollEvent>"
},
"docs": {
"text": "update on delta in case we don't know existing position or external change",
"tags": [{
"name": "param",
"text": "e"
}]
}
}
}; }
static get elementRef() { return "horizontalScroll"; }
}