@revolist/revogrid
Version:
Virtual reactive data grid spreadsheet component - RevoGrid.
598 lines (597 loc) • 25 kB
JavaScript
/*!
* Built by Revolist OU ❤️
*/
import { h, Host, } from "@stencil/core";
import GridResizeService from "../revoGrid/viewport.resize.service";
import LocalScrollService from "../../services/local.scroll.service";
import { LocalScrollTimer } from "../../services/local.scroll.timer";
import { CONTENT_SLOT, FOOTER_SLOT, HEADER_SLOT, } from "../revoGrid/viewport.helpers";
/**
* Viewport scroll component for RevoGrid
* @slot - content
* @slot header - header
* @slot footer - footer
*/
export class RevogrViewportScroll {
constructor() {
this.rowHeader = undefined;
this.contentWidth = 0;
this.contentHeight = 0;
this.colType = undefined;
}
async setScroll(e) {
var _a;
this.localScrollTimer.latestScrollUpdate(e.dimension);
(_a = this.localScrollService) === 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, silent = false) {
var _a, _b;
if (silent) {
if (e.coordinate && this.verticalScroll) {
switch (e.dimension) {
// for mobile devices to skip negative scroll loop. only on vertical scroll
case 'rgRow':
this.verticalScroll.style.transform = `translateY(${-1 * e.coordinate}px)`;
break;
}
}
return;
}
if (e.delta) {
switch (e.dimension) {
case 'rgCol':
e.coordinate = this.horizontalScroll.scrollLeft + e.delta;
break;
case 'rgRow':
e.coordinate = ((_b = (_a = this.verticalScroll) === null || _a === void 0 ? void 0 : _a.scrollTop) !== null && _b !== void 0 ? _b : 0) + e.delta;
break;
}
this.setScroll(e);
}
return e;
}
/**
* Dispatch this event to trigger vertical mouse wheel from plugins
*/
mousewheelVertical({ detail: e, }) {
this.verticalMouseWheel(e);
}
/**
* Dispatch this event to trigger horizontal mouse wheel from plugins
*/
mousewheelHorizontal({ detail: e, }) {
this.horizontalMouseWheel(e);
}
/**
* Allows to use outside listener
*/
scrollApply({ detail: { type, coordinate }, }) {
this.applyOnScroll(type, coordinate, true);
}
connectedCallback() {
/**
* Bind scroll functions for farther usage
*/
// allow mousewheel for all devices including mobile
this.verticalMouseWheel = this.onVerticalMouseWheel.bind(this, 'rgRow', 'deltaY');
this.horizontalMouseWheel = this.onHorizontalMouseWheel.bind(this, 'rgCol', 'deltaX');
this.localScrollTimer = new LocalScrollTimer('ontouchstart' in document.documentElement ? 0 : 10);
/**
* Create local scroll service
*/
this.localScrollService = new LocalScrollService({
// to improve safari smoothnes on scroll
// skipAnimationFrame: isSafariDesktop(),
runScroll: e => this.scrollViewport.emit(e),
applyScroll: e => {
this.localScrollTimer.setCoordinate(e);
switch (e.dimension) {
case 'rgCol':
// this will trigger on scroll event
this.horizontalScroll.scrollLeft = e.coordinate;
break;
case 'rgRow':
if (this.verticalScroll) {
// this will trigger on scroll event
this.verticalScroll.scrollTop = e.coordinate;
// for mobile devices to skip negative scroll loop. only on vertical scroll
if (this.verticalScroll.style.transform) {
this.verticalScroll.style.transform = '';
}
}
break;
}
},
});
}
componentDidLoad() {
// track viewport resize
this.resizeService = new GridResizeService(this.horizontalScroll, (entry) => {
var _a, _b, _c, _d, _e, _f, _g, _h;
const els = {};
let calculatedHeight = entry.height || 0;
if (calculatedHeight) {
calculatedHeight -=
((_b = (_a = this.header) === null || _a === void 0 ? void 0 : _a.clientHeight) !== null && _b !== void 0 ? _b : 0) +
((_d = (_c = this.footer) === null || _c === void 0 ? void 0 : _c.clientHeight) !== null && _d !== void 0 ? _d : 0);
}
els.rgRow = {
size: calculatedHeight,
contentSize: this.contentHeight,
scroll: (_f = (_e = this.verticalScroll) === null || _e === void 0 ? void 0 : _e.scrollTop) !== null && _f !== void 0 ? _f : 0,
noScroll: false,
};
const calculatedWidth = entry.width || 0;
els.rgCol = {
size: calculatedWidth,
contentSize: this.contentWidth,
scroll: this.horizontalScroll.scrollLeft,
noScroll: this.colType !== 'rgCol',
};
// Process changes in order: width first, then height
const dimensions = ['rgCol', 'rgRow'];
for (const dimension of dimensions) {
const item = els[dimension];
if (!item)
continue;
this.resizeViewport.emit({
dimension,
size: item.size,
rowHeader: this.rowHeader,
});
if (item.noScroll) {
continue;
}
(_g = this.localScrollService) === null || _g === void 0 ? void 0 : _g.scroll((_h = item.scroll) !== null && _h !== void 0 ? _h : 0, 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
switch (type) {
case 'rgCol':
el = this.horizontalScroll;
break;
case 'rgRow':
el = this.verticalScroll;
break;
}
// based on scroll visibility assign or remove class and event
if (hasScroll) {
el === null || el === void 0 ? void 0 : el.classList.add(`scroll-${type}`);
}
else {
el === null || el === void 0 ? void 0 : el.classList.remove(`scroll-${type}`);
}
this.scrollchange.emit({ type, hasScroll });
}
disconnectedCallback() {
var _a;
(_a = this.resizeService) === null || _a === void 0 ? void 0 : _a.destroy();
}
async componentDidRender() {
var _a, _b, _c, _d;
this.localScrollService.setParams({
contentSize: this.contentHeight,
clientSize: (_b = (_a = this.verticalScroll) === null || _a === void 0 ? void 0 : _a.clientHeight) !== null && _b !== void 0 ? _b : 0,
virtualSize: 0,
}, 'rgRow');
this.localScrollService.setParams({
contentSize: this.contentWidth,
clientSize: this.horizontalScroll.clientWidth,
virtualSize: 0,
}, 'rgCol');
this.setScrollVisibility('rgRow', (_d = (_c = this.verticalScroll) === null || _c === void 0 ? void 0 : _c.clientHeight) !== null && _d !== void 0 ? _d : 0, this.contentHeight);
this.setScrollVisibility('rgCol', this.horizontalScroll.clientWidth, this.contentWidth);
}
render() {
return (h(Host, { key: '09545ebf834794ac525565fb39b4c097e6dd6aa6', onWheel: this.horizontalMouseWheel, onScroll: (e) => this.applyScroll('rgCol', e) }, h("div", { key: 'f407a5b9ed9ca3ced107efb3a80a4eb0db92cefc', class: "inner-content-table", style: { width: `${this.contentWidth}px` } }, h("div", { key: 'cd01ed9ae19a4bbf2616e3a69435674ef624ca1f', class: "header-wrapper", ref: e => (this.header = e) }, h("slot", { key: '1673f06c5d5fafed2dec3a3fc5468cdf31490988', name: HEADER_SLOT })), h("div", { key: '9330984fa03ef2d323a8921ddb9b3c0e19596099', class: "vertical-inner", ref: el => (this.verticalScroll = el), onWheel: this.verticalMouseWheel, onScroll: (e) => this.applyScroll('rgRow', e) }, h("div", { key: 'cbb7fbb938e1d38e3572a3f6b30143cbf76fff7f', class: "content-wrapper", style: { height: `${this.contentHeight}px` } }, h("slot", { key: '592d4a7cca591cfb37b4828388fb8d63b3c52054', name: CONTENT_SLOT }))), h("div", { key: '7a69a5f8cce686efdf2a90a0cca386262c2db3bb', class: "footer-wrapper", ref: e => (this.footer = e) }, h("slot", { key: 'fceccd7653e0171ef52d5974fa1e465412bcfc08', 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
*/
async applyScroll(type, e) {
if (!(e.target instanceof HTMLElement)) {
return;
}
let scroll = 0;
switch (type) {
case 'rgCol':
scroll = e.target.scrollLeft;
break;
case 'rgRow':
scroll = e.target.scrollTop;
break;
}
// for mobile devices to skip negative scroll loop
if (scroll < 0) {
this.silentScroll.emit({ dimension: type, coordinate: scroll });
return;
}
this.applyOnScroll(type, scroll);
}
/**
* Applies change on scroll event only if mousewheel event happened some time ago
*/
applyOnScroll(type, coordinate, outside = false) {
const lastScrollUpdate = () => {
var _a;
(_a = this.localScrollService) === null || _a === void 0 ? void 0 : _a.scroll(coordinate, type, undefined, undefined, outside);
};
// apply after throttling
if (this.localScrollTimer.isReady(type, coordinate)) {
lastScrollUpdate();
}
else {
this.localScrollTimer.throttleLastScrollUpdate(type, coordinate, () => lastScrollUpdate());
}
}
/**
* On vertical mousewheel event
* @param type
* @param delta
* @param e
*/
onVerticalMouseWheel(type, delta, e) {
var _a, _b, _c, _d, _e, _f, _g, _h;
const scrollTop = (_b = (_a = this.verticalScroll) === null || _a === void 0 ? void 0 : _a.scrollTop) !== null && _b !== void 0 ? _b : 0;
const clientHeight = (_d = (_c = this.verticalScroll) === null || _c === void 0 ? void 0 : _c.clientHeight) !== null && _d !== void 0 ? _d : 0;
const scrollHeight = (_f = (_e = this.verticalScroll) === null || _e === void 0 ? void 0 : _e.scrollHeight) !== null && _f !== void 0 ? _f : 0;
// Detect if the user has reached the bottom
const atBottom = scrollTop + clientHeight >= scrollHeight && e.deltaY > 0;
const atTop = scrollTop === 0 && e.deltaY < 0;
if (!atBottom && !atTop) {
(_g = e.preventDefault) === null || _g === void 0 ? void 0 : _g.call(e);
}
const pos = scrollTop + e[delta];
(_h = this.localScrollService) === null || _h === void 0 ? void 0 : _h.scroll(pos, type, undefined, e[delta]);
this.localScrollTimer.latestScrollUpdate(type);
}
/**
* On horizontal mousewheel event
* @param type
* @param delta
* @param e
*/
onHorizontalMouseWheel(type, delta, e) {
var _a, _b;
if (!e.deltaX) {
return;
}
const { scrollLeft, scrollWidth, clientWidth } = this.horizontalScroll;
// Detect if the user has reached the right end
const atRight = scrollLeft + clientWidth >= scrollWidth && e.deltaX > 0;
// Detect if the user has reached the left end
const atLeft = scrollLeft === 0 && e.deltaX < 0;
if (!atRight && !atLeft) {
(_a = e.preventDefault) === null || _a === void 0 ? void 0 : _a.call(e);
}
const pos = scrollLeft + e[delta];
(_b = this.localScrollService) === null || _b === void 0 ? void 0 : _b.scroll(pos, type, undefined, e[delta]);
this.localScrollTimer.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 {
"rowHeader": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Enable row header"
},
"getter": false,
"setter": false,
"attribute": "row-header",
"reflect": false
},
"contentWidth": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Width of inner content"
},
"getter": false,
"setter": false,
"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"
},
"getter": false,
"setter": false,
"attribute": "content-height",
"reflect": false,
"defaultValue": "0"
},
"colType": {
"type": "string",
"mutable": false,
"complexType": {
"original": "DimensionCols | 'rowHeaders'",
"resolved": "\"colPinEnd\" | \"colPinStart\" | \"rgCol\" | \"rowHeaders\"",
"references": {
"DimensionCols": {
"location": "import",
"path": "@type",
"id": "src/types/index.ts::DimensionCols"
}
}
},
"required": true,
"optional": false,
"docs": {
"tags": [],
"text": ""
},
"getter": false,
"setter": false,
"attribute": "col-type",
"reflect": false
}
};
}
static get events() {
return [{
"method": "scrollViewport",
"name": "scrollviewport",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Before scroll event"
},
"complexType": {
"original": "ViewPortScrollEvent",
"resolved": "{ dimension: DimensionType; coordinate: number; delta?: number | undefined; outside?: boolean | undefined; }",
"references": {
"ViewPortScrollEvent": {
"location": "import",
"path": "@type",
"id": "src/types/index.ts::ViewPortScrollEvent"
}
}
}
}, {
"method": "resizeViewport",
"name": "resizeviewport",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Viewport resize"
},
"complexType": {
"original": "ViewPortResizeEvent",
"resolved": "{ dimension: DimensionType; size: number; rowHeader?: boolean | undefined; }",
"references": {
"ViewPortResizeEvent": {
"location": "import",
"path": "@type",
"id": "src/types/index.ts::ViewPortResizeEvent"
}
}
}
}, {
"method": "scrollchange",
"name": "scrollchange",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Triggered on scroll change, can be used to get information about scroll visibility"
},
"complexType": {
"original": "{\n type: DimensionType;\n hasScroll: boolean;\n }",
"resolved": "{ type: DimensionType; hasScroll: boolean; }",
"references": {
"DimensionType": {
"location": "import",
"path": "@type",
"id": "src/types/index.ts::DimensionType"
}
}
}
}, {
"method": "silentScroll",
"name": "scrollviewportsilent",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Silently scroll to coordinate\nMade to align negative coordinates for mobile devices"
},
"complexType": {
"original": "ViewPortScrollEvent",
"resolved": "{ dimension: DimensionType; coordinate: number; delta?: number | undefined; outside?: boolean | undefined; }",
"references": {
"ViewPortScrollEvent": {
"location": "import",
"path": "@type",
"id": "src/types/index.ts::ViewPortScrollEvent"
}
}
}
}];
}
static get methods() {
return {
"setScroll": {
"complexType": {
"signature": "(e: ViewPortScrollEvent) => Promise<void>",
"parameters": [{
"name": "e",
"type": "{ dimension: DimensionType; coordinate: number; delta?: number | undefined; outside?: boolean | undefined; }",
"docs": ""
}],
"references": {
"Promise": {
"location": "global",
"id": "global::Promise"
},
"ViewPortScrollEvent": {
"location": "import",
"path": "@type",
"id": "src/types/index.ts::ViewPortScrollEvent"
}
},
"return": "Promise<void>"
},
"docs": {
"text": "",
"tags": []
}
},
"changeScroll": {
"complexType": {
"signature": "(e: ViewPortScrollEvent, silent?: boolean) => Promise<ViewPortScrollEvent | undefined>",
"parameters": [{
"name": "e",
"type": "{ dimension: DimensionType; coordinate: number; delta?: number | undefined; outside?: boolean | undefined; }",
"docs": ""
}, {
"name": "silent",
"type": "boolean",
"docs": ""
}],
"references": {
"Promise": {
"location": "global",
"id": "global::Promise"
},
"ViewPortScrollEvent": {
"location": "import",
"path": "@type",
"id": "src/types/index.ts::ViewPortScrollEvent"
}
},
"return": "Promise<ViewPortScrollEvent | undefined>"
},
"docs": {
"text": "update on delta in case we don't know existing position or external change",
"tags": [{
"name": "param",
"text": "e"
}]
}
},
"applyScroll": {
"complexType": {
"signature": "(type: DimensionType, e: UIEvent) => Promise<void>",
"parameters": [{
"name": "type",
"type": "\"rgCol\" | \"rgRow\"",
"docs": ""
}, {
"name": "e",
"type": "UIEvent",
"docs": ""
}],
"references": {
"Promise": {
"location": "global",
"id": "global::Promise"
},
"DimensionType": {
"location": "import",
"path": "@type",
"id": "src/types/index.ts::DimensionType"
},
"UIEvent": {
"location": "global",
"id": "global::UIEvent"
}
},
"return": "Promise<void>"
},
"docs": {
"text": "Extra layer for scroll event monitoring, where MouseWheel event is not passing\nWe need to trigger scroll event in case there is no mousewheel event",
"tags": []
}
}
};
}
static get elementRef() { return "horizontalScroll"; }
static get listeners() {
return [{
"name": "mousewheel-vertical",
"method": "mousewheelVertical",
"target": undefined,
"capture": false,
"passive": false
}, {
"name": "mousewheel-horizontal",
"method": "mousewheelHorizontal",
"target": undefined,
"capture": false,
"passive": false
}, {
"name": "scroll-coordinate",
"method": "scrollApply",
"target": undefined,
"capture": false,
"passive": false
}];
}
}
//# sourceMappingURL=revogr-viewport-scroll.js.map