zul/src/main/resources/web/js/zul/mesh/Frozen.ts
/* Frozen.ts
Purpose:
Description:
History:
Wed Sep 2 10:07:04 2009, Created by jumperchen
Copyright (C) 2009 Potix Corporation. All Rights Reserved.
This program is distributed under LGPL Version 2.0 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
*/
// Bug 3218078
function _onSizeLater(wgt: Frozen): void {
var parent = wgt.parent!;
// ZK-2130: should skip fake scroll bar
if (parent.eheadtbl && parent._nativebar) {
var cells = parent._getFirstRowCells(parent.eheadrows)!,
head = parent.head!,
totalcols = cells.length - jq(head).find(head.$n_('bar')).length,
columns = wgt._columns!,
leftWidth = 0;
//B70-ZK-2553: one may specify frozen without any real column
if (totalcols <= 0) {
//no need to do the following computation since there is no any column
return;
}
//ZK-2776: don't take hidden column, like setVisible(false), into account
for (var header = parent.head!.firstChild; header; header = header.nextSibling) {
if (!header.isVisible())
totalcols -= 1;
}
for (var i = 0; i < columns; i++)
leftWidth += cells[i].offsetWidth;
parent._deleteFakeRow(parent.eheadrows);
wgt.$n_('cave').style.width = jq.px0(leftWidth);
var scroll = wgt.$n_('scrollX'),
width = parent.$n_('body').offsetWidth;
// B70-ZK-2074: Resize forzen's width as meshwidget's body.
parent.$n_('frozen').style.width = jq.px0(width);
width -= leftWidth;
scroll.style.width = jq.px0(width);
var scrollScale = totalcols - columns - 1;
(scroll.firstChild as HTMLElement).style.width = jq.px0(width + 50 * scrollScale);
wgt.syncScroll();
}
}
/**
* A frozen component to represent a frozen column or row in grid, like MS Excel.
* @defaultValue {@link getZclass}: z-frozen.
*/
@zk.WrapClass('zul.mesh.Frozen')
export class Frozen extends zul.Widget {
// Parent could be null because it's checked in `Frozen.prototype.syncScroll`.
override parent!: zul.mesh.MeshWidget | undefined;
/** @internal */
_start = 0;
/** @internal */
_scrollScale = 0;
/** @internal */
_smooth: boolean | undefined; // eslint-disable-line zk/preferStrictBooleanType
/** @internal */
_columns?: number;
/** @internal */
_shallSyncScale?: boolean;
/** @internal */
_delayedScroll?: number;
/** @internal */
_lastScale?: number;
/** @internal */
_shallSync?: boolean;
/**
* @returns the number of columns to freeze.
* @defaultValue `0`
*/
getColumns(): number | undefined {
return this._columns;
}
/**
* Sets the number of columns to freeze.(from left to right)
* @param columns - positive only
*/
setColumns(columns: number, opts?: Record<string, boolean>): this {
const o = this._columns;
columns = Math.max(0, columns);
this._columns = columns;
if (o !== columns || opts?.force) {
if (this._columns) {
if (this.desktop) {
this.onSize();
this.syncScroll();
}
} else this.rerender();
}
return this;
}
/**
* @returns the start position of the scrollbar.
* @defaultValue `0`
*/
getStart(): number {
return this._start;
}
/**
* Sets the start position of the scrollbar.
* @defaultValue `0`
* @param start - the column number
*/
setStart(start: number, opts?: Record<string, boolean>): this {
const o = this._start;
this._start = start;
if (o !== start || opts?.force) {
this.syncScroll();
}
return this;
}
/**
* Synchronizes the scrollbar according to {@link getStart}.
*/
syncScroll(): void {
if (this.parent?._nativebar) {
var scroll = this.$n('scrollX');
if (scroll)
scroll.scrollLeft = this._start * 50;
}
}
/**
* Synchronizes the scrollbar according to parent ebody scrollleft.
*/
syncScrollByParentBody(): void {
var p = this.parent,
ebody: HTMLDivElement | undefined,
l: number;
if (p?._nativebar && (ebody = p.ebody) && (l = ebody.scrollLeft) > 0) {
var scroll = this.$n('scrollX');
if (scroll) {
var scrollScale = l / (ebody.scrollWidth - ebody.clientWidth);
scroll.scrollLeft = Math.ceil(scrollScale * (scroll.scrollWidth - scroll.clientWidth));
}
}
}
/** @internal */
override bind_(desktop?: zk.Desktop, skipper?: zk.Skipper, after?: CallableFunction[]): void {
super.bind_(desktop, skipper, after);
var p = this.parent!,
body = p.$n('body'),
foot = p.$n('foot');
if (p._nativebar) {
//B70-ZK-2130: No need to reset when beforeSize, ZK-343 with native bar works fine too.
zWatch.listen({ onSize: this });
var scroll = this.$n_('scrollX'),
scrollbarWidth = jq.scrollbarWidth();
// ZK-2583: native IE bug, add 1px in scroll div's height for workaround
this.$n_().style.height = this.$n_('cave').style.height = this.$n_('right').style.height = scroll.style.height
= (scroll.firstChild as HTMLElement).style.height = jq.px0(scrollbarWidth);
p._currentLeft = 0;
this.domListen_(scroll, 'onScroll');
var head = p.$n('head');
if (head)
this.domListen_(head, 'onScroll', '_doHeadScroll');
} else {
// Bug ZK-2264
this._shallSyncScale = true;
}
// refix-ZK-3100455 : grid/listbox with frozen trigger "invalidate" should _syncFrozenNow
zWatch.listen({ onResponse: this });
if (body)
jq(body).addClass('z-word-nowrap');
if (foot)
jq(foot).addClass('z-word-nowrap');
}
/** @internal */
override unbind_(skipper?: zk.Skipper, after?: CallableFunction[], keepRod?: boolean): void {
var p = this.parent!,
body = p.$n('body'),
foot = p.$n('foot'),
head = p.$n('head');
if (p._nativebar) {
this.domUnlisten_(this.$n_('scrollX'), 'onScroll');
p.unlisten({ onScroll: this.proxy(this._onScroll) });
zWatch.unlisten({ onSize: this });
if (head)
this.domUnlisten_(head, 'onScroll', '_doHeadScroll');
} else {
this._shallSyncScale = false;
}
// refix-ZK-3100455 : grid/listbox with frozen trigger "invalidate" should _syncFrozenNow
zWatch.unlisten({ onResponse: this });
if (body)
jq(body).removeClass('z-word-nowrap');
if (foot)
jq(foot).removeClass('z-word-nowrap');
super.unbind_(skipper, after, keepRod);
}
// Bug ZK-2264, we should resync the variable of _scrollScale, which do the same as HeadWidget.js
onResponse(): void {
if (this.parent!._nativebar) {
// refix-ZK-3100455 : grid/listbox with frozen trigger "invalidate" should _syncFrozenNow
this._syncFrozenNow();
} else if (this._shallSyncScale) {
var hdfaker = this.parent!.ehdfaker;
if (hdfaker) {
this._scrollScale = hdfaker.childNodes.length - this._columns! - 1;
}
this._shallSyncScale = false;
}
}
override onSize(): void {
if (!this._columns)
return;
this._syncFrozen(); // B65-ZK-1470
//B70-ZK-2129: prevent height changed by scrolling
var p = this.parent!,
phead = p.head,
firstHdcell: HTMLElement | undefined;
if (p._nativebar && phead) {
//B70-ZK-2558: frozen will onSize before other columns,
//so there might be no any column in the beginning
var n = phead.$n() as (HTMLElement & Partial<Pick<HTMLTableRowElement, 'cells'>>) | undefined;
firstHdcell = n ? (n.cells ? n.cells[0] : undefined) : undefined;
//B70-ZK-2463: if firstHdcell is not undefined
if (firstHdcell) {
const fhcs = firstHdcell.style;
if (!fhcs.height || n!.cells![1]) {
fhcs.height = jq.px0(Math.max(firstHdcell.offsetHeight, n!.cells![1] ? n!.cells![1].offsetHeight : 0));
}
}
}
// Bug 3218078, to do the sizing after the 'setAttr' command
setTimeout(() => {
_onSizeLater(this);
this._syncFrozenNow();
});
}
/** @internal */
_syncFrozen(): void { //called by Rows, HeadWidget...
this._shallSync = true;
}
/** @internal */
_syncFrozenNow(): void {
var num = this._start;
if (this._shallSync && num)
this._doScrollNow(num, true);
this._shallSync = false;
}
/** @internal */
override beforeParentChanged_(p: zk.Widget | undefined): void {
//bug B50-ZK-238
//ZK-2651: JS Error showed when clear grid children component that include frozen
if (this.desktop && this._lastScale) //if large then 0
this._doScroll(0);
super.beforeParentChanged_(p);
}
/** @internal */
_onScroll(evt: zk.Event): void {
if (!evt.data || !zk.currentFocus)
return;
var p = this.parent,
td: HTMLTableCellElement | undefined,
fn = (): void => { // p shouldn't be null when fn is called
var cf = zk.currentFocus;
if (cf) {
td = p!.getFocusCell(cf.$n_());
var index: number;
if (td && (index = td.cellIndex - this._columns!) >= 0) {
this.setStart(index);
p!.ebody!.scrollLeft = 0;
if (p!.ehead)
p!.ehead.scrollLeft = 0;
}
}
};
if (p) {
fn();
}
evt.stop();
}
/** @internal */
_doHeadScroll(evt: zk.Event): void {
var head = evt.domTarget,
num = Math.ceil(head.scrollLeft / 50);
// ignore scrollLeft is 0
if (!head.scrollLeft || this._lastScale == num)
return;
evt.data = head.scrollLeft;
this._onScroll(evt);
}
/** @internal */
_doScroll(n: number): void {
var p = this.parent!,
num: number;
if (p._nativebar)
num = Math.ceil(this.$n_('scrollX').scrollLeft / 50);
else
num = Math.ceil(n);
if (this._lastScale == num)
return;
if (this._delayedScroll) {
clearTimeout(this._delayedScroll);
}
this._delayedScroll = setTimeout(() => {
this._lastScale = num;
this._doScrollNow(num);
this.smartUpdate('start', num);
this._start = num;
this._delayedScroll = undefined;
}, 0);
}
/** @internal */
_doScrollNow(num: number, force?: boolean): void {
var totalWidth = 0,
mesh = this.parent!,
cnt = num,
c = this._columns!,
width0 = zul.mesh.MeshWidget.WIDTH0,
hasVScroll = zk(mesh.ebody).hasVScroll(),
scrollbarWidth = hasVScroll ? jq.scrollbarWidth() : 0;
if (mesh.head) {
// set fixed size
var totalCols = mesh.head.nChildren,
// B70-ZK-2071: Use mesh.head to get columns.
hdcells = mesh.head.$n_().cells,
hdcol = mesh.ehdfaker!.firstChild,
ftrows = mesh.foot ? mesh.efootrows : undefined,
ftcells = ftrows ? ftrows.rows[0].cells : undefined;
for (var faker: HTMLElement | undefined, i = 0; hdcol && i < totalCols; hdcol = hdcol.nextSibling, i++) {
if (!(hdcol as HTMLElement).style.width.includes('px')) {
var sw = (hdcol as HTMLElement).style.width = jq.px0(hdcells[i].offsetWidth),
wgt = zk.Widget.$(hdcol)!;
if (!(wgt instanceof zul.mesh.HeadWidget)) {
if ((faker = wgt.$n('bdfaker')))
faker.style.width = sw;
if ((faker = wgt.$n('ftfaker')))
faker.style.width = sw;
}
}
}
interface Update {
node: HTMLTableCellElement;
index: number;
width?: string;
}
var updateBatch: Update[] = [], isVisible = false;
// B70-ZK-2071: Use mesh.head to get column.
for (var i = c, faker: HTMLElement | undefined; i < totalCols; i++) {
var n = hdcells[i],
hdWgt = zk.Widget.$<zul.mesh.HeaderWidget>(n)!,
shallUpdate = false,
cellWidth: string | undefined;
isVisible = hdWgt && hdWgt.isVisible();
//ZK-2776, once a column is hidden, there is an additional style
if (!hdWgt.isVisible())
continue; //skip column which is hide
if (cnt-- <= 0) { //show
var wd = isVisible ?
n.offsetWidth // Bug ZK-2690
: 0;
// ZK-2071: nativebar behavior should be same as fakebar
// ZK-4762: cellWidth should update while scroll into view
if (force || (wd < 2)) {
cellWidth = hdWgt._origWd || jq.px(wd);
// ZK-2772: consider faker's width first for layout consistent
// if the column is visible.
if ((wd > 1) && (faker = jq('#' + n.id + '-hdfaker')[0]) && faker.style.width)
cellWidth = faker.style.width;
hdWgt._origWd = undefined;
shallUpdate = true;
}
} else if (force ||
// Bug ZK-2690
(n.offsetWidth != 0)) { //hide
faker = jq('#' + n.id + '-hdfaker')[0];
//ZK-2776: consider faker's width first for layout consistent
if (faker.style.width && zk.parseInt(faker.style.width) > 1)
hdWgt._origWd = faker.style.width;
cellWidth = width0;
shallUpdate = true;
}
if (force || shallUpdate) {
updateBatch.push({ node: n, index: i, width: cellWidth });
}
}
//hide the element without losing focus
jq(mesh).css({ position: 'absolute', left: -9999 });
var update: Update | undefined;
while ((update = updateBatch.shift())) {
const n = update.node,
cellWidth = update.width!,
i = update.index;
if ((faker = jq('#' + n.id + '-hdfaker')[0]))
faker.style.width = cellWidth;
if ((faker = jq('#' + n.id + '-bdfaker')[0]) && isVisible)
faker.style.width = cellWidth;
if ((faker = jq('#' + n.id + '-ftfaker')[0]))
faker.style.width = cellWidth;
// ZK-2071: display causes wrong in colspan case
hdcells[i].style.width = cellWidth;
// foot
if (ftcells) {
// ZK-2071: display causes wrong in colspan case
if (ftcells.length > i)
ftcells[i].style.width = cellWidth;
}
}
hdcol = mesh.ehdfaker!.firstChild;
for (var i = 0; hdcol && i < totalCols; hdcol = hdcol.nextSibling, i++) {
if ((hdcol as HTMLElement).style.display != 'none')
totalWidth += zk.parseInt((hdcol as HTMLElement).style.width);
}
totalWidth += scrollbarWidth;
//hide the element without losing focus
jq(mesh).css({ position: '', left: '' });
}
// NOTE: Set style width to table to avoid colgroup width not working
// because of width attribute (width="100%") on table
const { eheadtbl, ebodytbl, efoottbl } = mesh;
if (eheadtbl)
eheadtbl.style.width = jq.px(totalWidth);
if (ebodytbl)
ebodytbl.style.width = jq.px(totalWidth - scrollbarWidth);
if (efoottbl)
efoottbl.style.width = jq.px(totalWidth);
mesh._restoreFocus();
}
}