/* MeshWidget.ts

	Purpose:

	Description:

	History:
		Sat May  2 09:36:31     2009, Created by tomyeh

Copyright (C) 2009 Potix Corporation. All Rights Reserved.

This program is distributed under LGPL Version 2.1 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
*/
/** The two-dimensional mesh related widgets.
 * A mesh widget is a two-dimensional widgets, such as grid, listbox and tree.
 * Classes in this package is the skeletal implementation that can be used
 * to simplify the implementation of grid, listbox and tree.
 */
//zk.$package('zul.mesh');
export interface MeshWidth {
	width: number;
	wds: number[];
}

var _shallFocusBack: boolean;
function _calcMinWd(wgt: MeshWidget): MeshWidth {
	var wgtn = wgt.$n(),
		ws = wgtn ? wgtn.style.whiteSpace : ''; //bug#3106514: sizedByContent with not visible columns
	if (wgtn) {
		wgtn.style.whiteSpace = 'nowrap'; // B50-3316030, B50-3346235: pre cause extra space
	}
	var eheadtblw: string | undefined,
		efoottblw: string | undefined,
		ebodytblw: string | undefined,
		eheadtblfix: string | undefined,
		efoottblfix: string | undefined,
		ebodytblfix: string | undefined,
		hdfaker = wgt.ehdfaker,
		bdfaker = wgt.ebdfaker,
		ftfaker = wgt.eftfaker,
		head = wgt.head,
		headn = head ? head.$n() : undefined,
		hdfakerws: string[] = [],
		bdfakerws: string[] = [],
		ftfakerws: string[] = [],
		hdws: string[] = [],
		hdcavews: string[] = [];

	if (wgt.eheadtbl && headn) {//clear and backup headers widths
		eheadtblw = wgt.getInnerWidth() || '';
		wgt.eheadtbl.width = '';
		wgt.eheadtbl.style.width = '';
		eheadtblfix = wgt.eheadtbl.style.tableLayout;
		wgt.eheadtbl.style.tableLayout = '';
		if (zk.chrome)
			wgt.ebodytbl!.style.display = 'block';
		var headcol = hdfaker!.firstChild,
			headcell = headn.firstChild;
		for (var i = 0; headcol; headcol = headcol.nextSibling, i++) {
			var headcave = headcell ? headcell.firstChild : undefined;
			if (!headcave)
				continue;
			hdfakerws[i] = (headcol as HTMLElement).style.width;
			(headcol as HTMLElement).style.width = '';
			hdws[i] = (headcell as HTMLElement).style.width;
			(headcell as HTMLElement).style.width = '';
			hdcavews[i] = (headcave as HTMLElement).style.width;
			(headcave as HTMLElement).style.width = '';

			headcell = headcell!.nextSibling;
		}
	}
	if (headn)
		headn.style.width = '';
	if (wgt.efoottbl) {//clear and backup footers widths
		efoottblw = eheadtblw || wgt.efoottbl.width;
		wgt.efoottbl.width = '';
		wgt.efoottbl.style.width = '';
		efoottblfix = wgt.efoottbl.style.tableLayout;
		wgt.efoottbl.style.tableLayout = '';
		if (zk.chrome)
			wgt.ebodytbl!.style.display = 'block';
		if (ftfaker) {
			var footcol = ftfaker.firstChild,
				ftrows = wgt.efootrows,
				ftcells = ftrows ? ftrows.rows[0].cells : undefined;
			for (var i = 0; footcol; footcol = footcol.nextSibling) {
				if (ftcells?.[i])
					ftcells[i].style.width = ''; // reset it

				ftfakerws[i++] = (footcol as HTMLElement).style.width;
				(footcol as HTMLElement).style.width = '';
			}
		}
	}
	if (wgt.ebodytbl) {//clear and backup body faker widths
		ebodytblw = eheadtblw || wgt.ebodytbl.width;
		wgt.ebodytbl.width = '';
		wgt.ebodytbl.style.width = '';
		ebodytblfix = wgt.ebodytbl.style.tableLayout;
		wgt.ebodytbl.style.tableLayout = '';
		if (zk.chrome)
			wgt.ebodytbl.style.display = 'block';
		if (bdfaker) {
			var bodycol = bdfaker.firstChild;
			for (var i = 0; bodycol; bodycol = bodycol.nextSibling) {
				bdfakerws[i++] = (bodycol as HTMLElement).style.width;
				(bodycol as HTMLElement).style.width = '';
			}
		}
	}

	//calculate widths
	var wds: number[] = [],
		width = 0,
		len = head ? head.nChildren : 0,
		w = head ? head.firstChild : undefined,
		headWgt = wgt.getHeadWidget();
	if (bdfaker && w) {
		var bodycells = wgt._getFirstRowCells(wgt.ebodyrows),
			footcells = ftfaker ? wgt._getFirstRowCells(wgt.efootrows) : undefined;

		for (var i = 0; i < len; i++) {
			var wd = bodycells?.[i] ? zk(bodycells[i]).offsetWidthCeil() : 0,
				ftwd = footcells?.[i] && zk(footcells[i]).isVisible() ? footcells[i].offsetWidth : 0,
				hdwd = w?.isVisible() ? w.getContentWidth_() : 0,
				header: zk.Widget | undefined;

			if ((header = headWgt!.getChildAt(i)) && header.getWidth())
				hdwd = Math.max(hdwd, ftwd);
			if (hdwd > wd)
				wd = hdwd;
			if (ftwd > wd)
				wd = ftwd;
			wds[i] = wd;
			// Bug ZK-2772 don't plus one when frozen exists.
			if (!wgt.frozen && ((zk.ff as number) > 4 || zk.safari)) // firefox4 & IE9, 10, 11 & safari still cause break line in case B50-3147926 column 1
				++wds[i];
			width += wds[i]; // using wds[i] instead of wd for B50-3183172.zul
			if (w)
				w = w.nextSibling;
		}
		wgt._deleteFakeRow(wgt.ebodyrows);
		if (ftfaker)
			wgt._deleteFakeRow(wgt.efootrows);
	} else {
		var tr = _getSigRow(wgt);
		if (tr) {
			for (var cells = tr.cells, i = cells.length; i--;) {
				var wd = cells[i].offsetWidth;
				wds[i] = wd;

				// Bug ZK-2772 don't plus one when frozen exists.
				if (!wgt.frozen && ((zk.ff as number) > 4)) // firefox4 & IE9, 10, 11 still cause break line in case B50-3147926 column 1
					++wds[i];
				width += wds[i]; // using wds[i] instead of wd for B50-3183172.zul
			}
		}
	}

	// ZK-3313: The width of empty message counts
	var empty = wgt.$n('empty');
	if (empty && empty.style.display != 'none') {
		var emptyWidth = empty.offsetWidth;
		if (emptyWidth > width)
			width = emptyWidth;
	}

	if (wgt.eheadtbl && headn) {//restore headers widths
		if (!eheadtblw!.includes('%')) { // once sized and consider faker bar
			var $hdfakerbar = jq(wgt.head!.$n_('hdfaker')).find('[id*=hdfaker-bar]'),
				hdfakerbar = $hdfakerbar[0];
			if (hdfakerbar) eheadtblw = jq.px0(parseInt(eheadtblw!) + parseInt(hdfakerbar.style.width));
			wgt.eheadtbl.style.width = eheadtblw!;
		} else
			wgt.eheadtbl.width = eheadtblw ?? '';

		wgt.eheadtbl.style.tableLayout = eheadtblfix ?? '';
		if (zk.chrome)
			wgt.eheadtbl.style.display = '';
		var headcol = hdfaker!.firstChild,
			headcell = headn.firstChild;
		for (var i = 0; headcol; headcol = headcol.nextSibling, i++) {
			var headcave = headcell ? headcell.firstChild : undefined;
			if (!headcave)
				continue;
			(headcol as HTMLElement).style.width = hdfakerws[i];
			(headcell as HTMLElement).style.width = hdws[i];
			(headcave as HTMLElement).style.width = hdcavews[i];

			headcell = headcell!.nextSibling;
		}
	}
	if (wgt.efoottbl) {//restore footers widths
		wgt.efoottbl.width = efoottblw ?? '';
		wgt.efoottbl.style.tableLayout = efoottblfix ?? '';
		if (zk.chrome)
			wgt.efoottbl.style.display = '';
		if (ftfaker) {
			var footcol = ftfaker.firstChild;
			for (var i = 0; footcol; footcol = footcol.nextSibling)
				(footcol as HTMLElement).style.width = ftfakerws[i++];
		}
	}
	if (wgt.ebodytbl) {//restore body fakers widths
		wgt.ebodytbl.width = ebodytblw ?? '';
		wgt.ebodytbl.style.tableLayout = ebodytblfix ?? '';
		if (zk.chrome)
			wgt.ebodytbl.style.display = '';
		if (bdfaker) {
			var bodycol = bdfaker.firstChild;
			for (var i = 0; bodycol; bodycol = bodycol.nextSibling)
				(bodycol as HTMLElement).style.width = bdfakerws[i++];
		}
	}

	if (wgtn)
		wgtn.style.whiteSpace = ws;
	return { width: width, wds: wds };
}
function _fixBodyMinWd(wgt: MeshWidget, fixMesh?: boolean): void {
	// effective only when there is no header
	var sbc = wgt.isSizedByContent(),
		meshmin = wgt._hflex == 'min';
	if (!wgt.head && (meshmin || sbc)) {
		var bdw = zk(wgt.$n()).padBorderWidth(),
			wd = _getMinWd(wgt) + bdw, // has to call _getMinWd first so wgt._minWd will be available
			// eslint-disable-next-line zk/noNull
			tr: ChildNode | null = wgt.ebodytbl!,
			wds = wgt._minWd!.wds,
			wlen = wds.length;
		if (fixMesh && meshmin)
			wgt.setFlexSize_({ width: wd }, true);
		if (!(tr.firstChild) || !(tr = tr.firstChild.firstChild))
			return; // no first tr
		// eslint-disable-next-line zk/noNull
		for (var c: ChildNode | null = tr.firstChild, i = 0; c && (i < wlen); c = c.nextSibling)
			(c as HTMLElement).style.width = jq.px(wds[i++]);
		if (sbc && !meshmin) {
			// add flex <td> if absent
			var bdfx = tr.lastChild,
				bdfxid = wgt.uuid + '-bdflex';
			if (!bdfx || (bdfx as HTMLElement).id != bdfxid) {
				jq(tr).append(/*safe*/ '<td id="' + bdfxid + '"></td>');
				bdfx = tr.lastChild;
			}
		}
	}
}
function _fixPageSize(wgt: MeshWidget, rows: HTMLCollectionOf<HTMLTableRowElement>): boolean {
	var ebody = wgt.ebody;
	if (!ebody)
		return false; //not ready yet
	var max = ebody.offsetHeight;
	if (zk(ebody).hasHScroll()) //with horizontal scrollbar
		max -= jq.scrollbarWidth();
	if (max == wgt._prehgh) return false; //same height, avoid fixing page size
	wgt._prehgh = max;
	var ebodytbl = wgt.ebodytbl!,
		etbparent = ebodytbl.offsetParent,
		etbtop = ebodytbl.offsetTop,
		hgh = 0,
		row: HTMLTableRowElement | undefined,
		j = 0;
	for (var it = wgt.getBodyWidgetIterator({ skipHidden: true }),
		len = rows.length; it.next() && j < len; j++) {
		row = rows[j];
		var top: number | undefined = row.offsetTop - (row.offsetParent == etbparent ? etbtop : 0);
		if (top > max) {
			--j;
			break;
		}
		hgh = top;
	}
	if (row) { //there is row
		var pgib = wgt.$n('pgib'),
			pgibHgh = 0;
		if (pgib)
			pgibHgh = pgib.offsetHeight;
		var withPgibMax = max + pgibHgh;
		if (top! <= withPgibMax) { //row not exceeds the height, estimate
			var paging = (wgt.paging) ? wgt.paging : wgt.getPaginal()!,
				totalsz = paging.getTotalSize(),
				rowsHgh = hgh + row.offsetHeight,
				j1 = Math.floor(j * max / rowsHgh),
				j2 = Math.floor(j * withPgibMax / rowsHgh);
			if (totalsz > j2 || !paging.isAutohide())
				j = j1;
			else
				j = j2;
		}
		//enforce pageSize change
		if (j == 0) j = 1; //at least one per page
		if (j != wgt.getPageSize()) {
			wgt.fire('onPageSize', { size: j });
			return true;
		}
	}
	return false;
}
function _adjMinWd(wgt: MeshWidget): void {
	if (wgt._hflex == 'min') {
		var w = _getMinWd(wgt),
			n = wgt.$n_();
		wgt._hflexsz = w + zk(n).padBorderWidth(); //override
		n.style.width = jq.px0(wgt._hflexsz);
	}
}
function _getMinWd(wgt: MeshWidget): number {
	wgt._calcMinWds();
	var bdfaker = wgt.ebdfaker,
		wd: number,
		wds: number[] = [],
		width: number,
		_minwds = wgt._minWd!.wds;
	if (wgt.head && bdfaker) {
		width = 0;
		var w = wgt.head.firstChild,
			bdcol = bdfaker.firstChild;

		for (var i = 0; w; w = w.nextSibling) {
			if (w._hflex == 'min')
				wd = wds[i] = _minwds[i] + zk(w.$n()).padBorderWidth();
			else {
				if (w._width && w._width.indexOf('px') > 0)
					wd = wds[i] = zk.parseInt(w._width);
				else
					wd = wds[i] = zk.parseInt((bdcol as HTMLElement).style.width);
			}
			// ZK-2130: should save the head width
			w._origWd = jq.px0(wd);
			width += wd;
			++i;
			bdcol = bdcol!.nextSibling;
		}
	} else
		width = wgt._minWd!.width; // no header
	return width + (zk(wgt.ebody).hasVScroll() ? jq.scrollbarWidth() : 0);
}
function _getSigRow(wgt: MeshWidget): HTMLTableRowElement | undefined {
	// scan for tr with largest number of td children
	var rw = wgt.getBodyWidgetIterator().next(),
		// eslint-disable-next-line zk/noNull
		tr = rw ? rw.$n() : null;
	if (!tr)
		return;
	// eslint-disable-next-line zk/noNull
	for (var maxtr = tr, len: number, max = maxtr.cells.length; tr; tr = tr.nextSibling as HTMLTableRowElement | null)
		if ((len = tr.cells.length) > max) {
			maxtr = tr;
			max = len;
		}
	return maxtr;
}
function _cpCellWd(wgt: MeshWidget): void {
	var dst = wgt.efootrows!.rows[0],
		srcrows = wgt.ebodyrows!.rows;
	if (!dst || !srcrows || !srcrows.length || !dst.cells.length)
		return;
	var ncols = dst.cells.length,
		// eslint-disable-next-line zk/noNull
		src: HTMLTableRowElement | null | undefined,
		maxnc = 0;
	for (var j = 0, it = wgt.getBodyWidgetIterator({ skipHidden: true }), w: zul.mesh.Item | undefined; (w = it.next());) {
		if (!w._loaded || w.z_rod)
			continue;

		var row = srcrows[j++], $row = zk(row),
			cells = row.cells, nc = $row.ncols(),
			valid = cells.length == nc && $row.isVisible();
		//skip with colspan and invisible
		if (valid && nc >= ncols) {
			maxnc = ncols;
			src = row;
			break;
		}
		if (nc > maxnc) {
			src = valid ? row : undefined;
			maxnc = nc;
		} else if (nc == maxnc && !src && valid) {
			src = row;
		}
	}
	if (!maxnc) return;

	var fakeRow = !src;
	if (fakeRow) { //the longest row containing colspan
		src = document.createElement('TR') as HTMLTableRowElement;
		src.style.height = '0px';
		//Note: we cannot use display="none" (offsetWidth won't be right)
		for (var j = 0; j < maxnc; ++j)
			src.appendChild(document.createElement('TD'));
		srcrows[0].parentNode!.appendChild(src);
	}
	//we have to clean up first, since, in FF, if dst contains %
	//the copy might not be correct
	for (var j = maxnc; j--;)
		dst.cells[j].style.width = '';

	var sum = 0;
	for (var j = maxnc; j--;) {
		var d = dst.cells[j], s = src!.cells[j];
		if (zk.opera) {
			sum += s.offsetWidth;
			d.style.width = zk(s).contentWidth() as unknown as string;
		} else {
			d.style.width = `${s.offsetWidth}px`;
			if (maxnc > 1) { //don't handle single cell case (bug 1729739)
				var v = s.offsetWidth - d.offsetWidth;
				if (v != 0) {
					v += s.offsetWidth;
					if (v < 0) v = 0;
					d.style.width = `${v}px`;
				}
			}
		}
	}
	if (zk.opera && wgt.isSizedByContent())
		(dst.parentNode!.parentNode as HTMLElement).style.width = `${sum}px`;
	if (fakeRow)
		src!.parentNode!.removeChild(src!);
}
function listenOnFitSize(wgt: MeshWidget): void {
	if (wgt._rows && !wgt._rowsOnFitSize) {
		zWatch.listen({ onFitSize: wgt });
		wgt._rowsOnFitSize = true;
	}
}
function unlistenOnFitSize(wgt: MeshWidget): void {
	if (wgt._rowsOnFitSize) {
		zWatch.unlisten({ onFitSize: wgt });
		delete wgt._rowsOnFitSize;
	}
}

export interface Item extends zul.Widget<HTMLTableRowElement> {
	/** @internal */
	_loaded?: boolean;
	/** @internal */
	_index?: number;
}

export interface ItemIterator<TItem extends zul.mesh.Item = zul.mesh.Item> {
	hasNext(): boolean;
	next(): TItem | undefined;
	length?: number;
}
/**
 *  A skeletal implementation for a mesh widget.
 *  @see zul.grid.Grid
 *  @see zul.sel.Tree
 *  @see zul.sel.Listbox
 */
@zk.WrapClass('zul.mesh.MeshWidget')
export abstract class MeshWidget extends zul.Widget {
	/** @internal */
	_rows = 0;
	/** @internal */
	_pagingPosition = 'bottom';
	/** @internal */
	_prehgh = -1;
	/** @internal */
	_minWd?: MeshWidth; //minimum width for each column
	/** @internal */
	_sizedByContent?: boolean;
	/** @internal */
	_span?: string | boolean;
	/** @internal */
	_nspan?: number;
	/** @internal */
	_autopaging?: boolean;
	/** @internal */
	_model?: boolean | string;
	/** @internal */
	_paginal?: zul.mesh.Paging;
	/** @internal */
	_pendOnRender?: boolean;
	/** @internal */
	_ebodyScrollPos?: { l: number; t: number };
	// Types established from inspecting https://www.zkoss.org/zkdemo/grid/header_and_footer
	ehead?: HTMLDivElement;
	eheadtbl?: HTMLTableElement;
	ehdfaker?: HTMLTableColElement;
	eheadrows?: HTMLTableSectionElement;
	// Types established from inspecting https://www.zkoss.org/zkdemo/grid/header_and_footer
	ebody?: HTMLDivElement;
	ebodytbl?: HTMLTableElement;
	ebdfaker?: HTMLTableColElement;
	ebodyrows?: HTMLTableSectionElement;
	// Types established from inspecting https://www.zkoss.org/zkdemo/grid/header_and_footer
	efoot?: HTMLDivElement;
	efoottbl?: HTMLTableElement;
	eftfaker?: HTMLTableColElement;
	efootrows?: HTMLTableSectionElement;

	efrozen?: HTMLElement;
	frozen?: zul.mesh.Frozen;

	paging?: zul.mesh.Paging;
	heads: zul.mesh.HeadWidget[];
	head?: zul.mesh.HeadWidget;
	foot?: zul.grid.Foot;
	/** @internal */
	_visiRows?: number;
	/** @internal */
	_wsbak?: string;
	/** @internal */
	_targetIndex?: number;
	/** @internal */
	_keepScroll?: boolean;
	/** @internal */
	_bottomBoundary?: number;
	/** @internal */
	_topBoundary?: number;
	/** @internal */
	_rowsOnFitSize?: boolean;
	/** @internal */
	_scrollbar?: zul.Scrollbar;
	/** @internal */
	_cachehgh?: number;
	/** @internal */
	_lastDevicePixelRatio?: number;
	/** @internal */
	_adjustScrollTopLater?: boolean;
	/** @internal */
	_shallSize?: boolean;
	/** @internal */
	_syncingbodyrows?: boolean;
	/** @internal */
	_shallClearTableWidth?: boolean;
	/** @internal */
	_shallShowScrollbar?: boolean;

	/** @internal */
	_syncEmpty(): void {
		// Empty on purpose. To be inherited.
	}

	constructor() {
		super(); // FIXME: arguments?
		this.heads = [];
	}

	// `zul.mesh.prototype.HeadWidget.onColSize` could assign `_innerWidth` a number
	/** @internal */
	_innerWidth = '100%';
	/** @internal */
	_currentTop = 0;
	/** @internal */
	_currentLeft = 0;
	/** @internal */
	_nativebar = true;

	abstract getHeadWidgetClass(): typeof zul.mesh.HeadWidget;
	abstract getBodyWidgetIterator(opts?: Record<string, unknown>): ItemIterator;
	abstract itemIterator(opts?: Record<string, unknown>): ItemIterator;

	/**
	 * @returns (int) the rows. Zero means no limitation.
	 * @defaultValue `0`.
	 */
	getRows(): number {
		return this._rows;
	}

	/**
	 * Sets the rows.
	 * <p>
	 * Note: if both {@link setHeight} is specified with non-empty,
	 * {@link setRows} is ignored
	 */
	setRows(rows: number, opts?: Record<string, boolean>): this {
		const o = this._rows;
		this._rows = rows;

		if (o !== rows || opts?.force) {
			listenOnFitSize(this);
			var n = this.$n();
			if (n) {
				n._lastsz = undefined;
				this.onSize();
			}
		}

		return this;
	}

	/**
	 * @returns how to position the paging of the widget at the client screen.
	 * It is meaningless if the mold is not in "paging".
	 */
	getPagingPosition(): string {
		return this._pagingPosition;
	}

	/**
	 * Sets how to position the paging of the widget at the client screen.
	 * It is meaningless if the mold is not in "paging".
	 * @param pagingPosition - how to position. It can only be "bottom" (the default), or
	 * "top", or "both".
	 */
	setPagingPosition(pagingPosition: string, opts?: Record<string, boolean>): this {
		const o = this._pagingPosition;
		this._pagingPosition = pagingPosition;

		if (o !== pagingPosition || opts?.force) {
			this.rerender();
		}

		return this;
	}

	/**
	 * Returns whether the paging component of this component is disabled
	 * @since 10.0.0
	 */
	isPagingDisabled(): boolean {
		return !!this.getPagingChild()?.isDisabled();
	}

	/**
	 * Sets whether to disable the Paging component of this component
	 * @since 10.0.0
	 */
	setPagingDisabled(pagingDisabled: boolean): this {
		this.getPagingChild()?.setDisabled(pagingDisabled);
		return this;
	}

	/**
	 * @returns whether sizing the widget column width by its content. Default is false.
	 * <p>Note: if the "sized-by-content" attribute of component is specified,
	 * it's prior to the original value.
	 * @see {@link setSizedByContent}
	 */
	isSizedByContent(): boolean {
		return !!this._sizedByContent;
	}

	/**
	 * Sets whether sizing the widget column width by its content. Default is false, i.e.
	 * the outline of the widget is dependent on browser. It means, we don't
	 * calculate the width of each cell. If set to true, the outline will count on
	 * the content of body. In other words, the outline of the widget will be like
	 * ZK version 2.4.1 that the header's width is only for reference.
	 *
	 * <p> You can also specify the "sized-by-content" attribute of component in
	 * lang-addon.xml directly, it will then take higher priority.
	 */
	setSizedByContent(sizedByContent: boolean, opts?: Record<string, boolean>): this {
		const o = this._sizedByContent;
		this._sizedByContent = sizedByContent;

		if (o !== sizedByContent || opts?.force) {
			this.rerender();
		}

		return this;
	}

	/**
	 * @returns column span hint of this widget.
	 * @defaultValue `null`
	 * @since 5.0.6
	 * @see {@link setSpan}
	 */
	getSpan(): string | boolean | undefined {
		return this._span;
	}
	isSpan(): string | boolean | undefined {
		return this._span;
	}

	/**
	 * Sets column span hint of this mesh widget.
	 * <p>The parameter span is a number in String type indicating how this
	 * component distributes remaining empty space to the
	 * specified column(0-based). "0" means distribute remaining empty space to the 1st column; "1" means
	 * distribute remaining empty space to the 2nd column, etc.. The spanning column will grow to
	 * fit the extra remaining space.</p>
	 * <p>Special span hint with "true" means span ALL columns proportionally per their
	 * original widths while null or "false" means NOT spanning any column.</p>
	 * @defaultValue `null`. That is, NOT span any column.</p>
	 * <p>Note span is meaningful only if there is remaining empty space for columns.</p>
	 *
	 * @param span - the column span hint.
	 * @since 5.0.6
	 * @see {@link getSpan}
	 */
	setSpan(span: string | boolean, opts?: Record<string, boolean>): this {
		const o = this._span;
		this._span = span;

		if (o !== span || opts?.force) {
			//ZK-2776: if span="false", !isSpan() will return false, because "false" is string not boolean
			var isTrue = true === span || 'true' == span,
				isFalse = false === span || 'false' == span;
			this._span = isTrue ? true : isFalse ? false : span;

			var x = isTrue ? -65500 : isFalse ? 0 : (zk.parseInt(span) + 1);
			this._nspan = x < 0 && x != -65500 ? 0 : x;
			this.rerender();
		}

		return this;
	}

	/**
	 * @returns whether turn on auto-paging facility when mold is
	 * "paging". If it is set to true, the {@link setPageSize} is ignored;
	 * rather, the page size(number of item count) is automatically determined by the
	 * height of this widget dynamically.
	 * @see {@link setAutopaging}
	 */
	isAutopaging(): boolean {
		return !!this._autopaging;
	}

	/**
	 * Sets whether turn on auto-paging facility when mold is
	 * "paging". If it is set to true, the {@link setPageSize} is ignored;
	 * rather, the page size(number of item count) is automatically determined by the
	 * height of this widget dynamically.
	 */
	setAutopaging(autopaging: boolean, opts?: Record<string, boolean>): this {
		const o = this._autopaging;
		this._autopaging = autopaging;

		if (o !== autopaging || opts?.force) {
			this.rerender();
		}

		return this;
	}

	/**
	 * @returns whether the widget is in model mode or not.
	 */
	isModel(): boolean {
		return !!this._model;
	}
	// TODO: Used by grid. Should prefer the use of isModel
	getModel(): boolean | string | undefined {
		return this._model; // it could be "group" for groups model
	}

	/**
	 * Sets whether the widget is in model mode.
	 */
	setModel(model: boolean): this {
		this._model = model;
		return this;
	}

	/**
	 * @returns the inner width of this component.
	 * The inner width is the width of the inner table.
	 * @defaultValue `"100%"`
	 * @see {@link setInnerWidth}
	 */
	getInnerWidth(): string {
		return this._innerWidth;
	}

	/**
	 * Sets the inner width of this component.
	 * The inner width is the width of the inner table.
	 * By default, it is 100%. That is, it is the same as the width
	 * of this component. However, it is changed when the user
	 * is sizing the column's width.
	 *
	 * <p>Application developers rarely call this method, unless
	 * they want to preserve the widths of sizable columns
	 * changed by the user.
	 * To preserve the widths, the developer have to store the widths of
	 * all columns and the inner width ({@link getInnerWidth}),
	 * and then restore them when re-creating this component.
	 *
	 * @param innerWidth - the inner width. If null, "100%" is assumed.
	 */
	setInnerWidth(innerWidth: string, opts?: Record<string, boolean>): this {
		const o = this._innerWidth;
		this._innerWidth = innerWidth;

		if (o !== innerWidth || opts?.force) {
			if (innerWidth == null) this._innerWidth = innerWidth = '100%';
			if (this.eheadtbl) this.eheadtbl.style.width = innerWidth;
			if (this.ebodytbl) this.ebodytbl.style.width = innerWidth;
			if (this.efoottbl) this.efoottbl.style.width = innerWidth;
		}

		return this;
	}

	/**
	 * @returns the external Paging widget, if any.
	 */
	getPaginal(): zul.mesh.Paging | undefined {
		return this._paginal;
	}

	/**
	 * Sets the external Paging widget.
	 */
	setPaginal(paginal?: zul.mesh.Paging): this {
		if (this._paginal != paginal) {
			this._paginal?.setMeshWidget();
			this._paginal = paginal;
			this._paginal?.setMeshWidget(this);
		}
		return this;
	}

	/**
	 * @returns the page size, aka., the number rows per page.
	 * @see {@link zul.mesh.Paging.getPageSize}
	 */
	getPageSize(): number {
		return this.getPagingChild()?.getPageSize() ?? 0;
	}

	/**
	 * Sets the page size, aka., the number rows per page.
	 * @see {@link zul.mesh.Paging.setPageSize}
	 */
	setPageSize(pageSize: number): this {
		this.getPagingChild()?.setPageSize(pageSize);
		return this;
	}

	/**
	 * @returns the number of pages.
	 * Note: there is at least one page even no item at all.
	 * @see {@link zul.mesh.Paging.getPageCount}
	 */
	getPageCount(): number {
		return this.getPagingChild()?.getPageCount() ?? 1;
	}

	/**
	 * @returns the active page (starting from 0).
	 * @see {@link zul.mesh.Paging.getActivePage}
	 */
	getActivePage(): number {
		return this.getPagingChild()?.getActivePage() ?? 0;
	}

	/**
	 * Sets the active page (starting from 0).
	 * @see {@link zul.mesh.Paging.setActivePage}
	 */
	setActivePage(activePage: number): this {
		this.getPagingChild()?.setActivePage(activePage);
		return this;
	}

	/**
	 * @returns whether the widget is in paging mold.
	 */
	inPagingMold(): boolean {
		return 'paging' == this.getMold();
	}

	getPagingChild(): zul.mesh.Paging | undefined {
		return this.paging ?? this.getPaginal();
	}

	override setHeight(height?: string): this {
		super.setHeight(height);
		if (this.desktop) {
			this._setHgh(height);
			this.onSize();
		}
		return this;
	}

	override setWidth(width?: string): this {
		super.setWidth(width);
		if (this.eheadtbl) this.eheadtbl.style.width = '';
		if (this.efoottbl) this.efoottbl.style.width = '';
		if (this.desktop)
			this.onSize();
		return this;
	}

	override setStyle(style: string): this {
		if (this._style != style) {
			super.setStyle(style);
			if (this.desktop)
				this.onSize();
		}
		return this;
	}

	/**
	 * @returns the self's head widget.
	 * @since 5.0.4
	 */
	getHeadWidget(): zul.mesh.HeadWidget | undefined {
		return this.head;
	}

	/**
	 * @param el - the element got focus.
	 * @returns the focused cell element.
	 * @since 5.0.7
	 */
	getFocusCell(el: HTMLElement): HTMLTableCellElement | undefined {
		var td: HTMLTableCellElement | undefined;
		// Note: `jq.each` is in general not the same as `jq().each` although it is in this case.
		// But, only `jq.each` matches the definition in `jQuery.d.ts`.
		// See https://api.jquery.com/jquery.each/#:~:text=%24.each()%20function-,is%20not%20the%20same%20as,-%24(selector).each()
		jq.each([this.ebodytbl, this.eheadtbl, this.efoottbl], function (i) {
			if (this && jq.isAncestor(this, el)) {
				var tds = jq(el).parents(i == 1 ? 'th' : 'td'); // headtable uses TH, others uses TD
				for (var i = 0, j = tds.length; i < j; i++) {
					td = tds[i];
					if (jq(td).parents('table')[0] == this) {
						return false; // break the loop;
					}
				}
			}
		});
		return td;
	}

	/** @internal */
	_moveToHidingFocusCell(index: number, ignoreWidth?: boolean, notFocusBack?: boolean): void { //used in Row/Listcell and a11y frozen
		//B50-3178977 navigating the input in hiddin column.
		var td = this.ehdfaker ? this.ehdfaker.childNodes[index] as HTMLTableCellElement | undefined : undefined,
			frozen = this.frozen;
		if (td && frozen) {
			const frozenCols = frozen.getColumns()!,
				scrollableIndex = index - frozenCols;

			if (scrollableIndex >= 0 && (ignoreWidth || this._isCellOutOfView(td, index))) {
				const bar = this._scrollbar;
				if (this._nativebar) {
					frozen.setStart(scrollableIndex, { force: true });
				} else if (bar) {
					frozen._doScrollNow(scrollableIndex);
					bar.setBarPosition(scrollableIndex);
				}

				_shallFocusBack = !notFocusBack;
			}
		}
	}

	/** @internal */
	_isCellOutOfView(td: HTMLTableCellElement, index: number): boolean {
		let isOutOfView = false;
		const ehead = this.ehead,
			frozenCols = this.frozen!.getColumns(),
			columnNodes = this.ehdfaker!.childNodes as NodeListOf<HTMLElement>;

		if (ehead && frozenCols) {
			let leftFrozenWidth = 0;
			for (let i = 0; i < frozenCols; i++) {
				leftFrozenWidth += columnNodes[i].offsetWidth;
			}

			const tdLeft = td.offsetLeft,
				tdRight = tdLeft + td.offsetWidth,
				viewportLeft = this._currentLeft + leftFrozenWidth,
				viewportRight = this._currentLeft + ehead.offsetWidth;

			isOutOfView = (tdLeft < viewportLeft) || (tdRight > viewportRight);
		}
		return isOutOfView;
	}

	/** @internal */
	_restoreFocus(): void { //used in Frozen
		if (_shallFocusBack && zk.currentFocus) {
			_shallFocusBack = false;
			zk.currentFocus.focus();
		}
	}

	/** @internal */
	disableAutoSizing_(): void {
		this._span = false;
		this._nspan = 0;
		this._sizedByContent = false;
	}

	/** @internal */
	override bind_(desktop?: zk.Desktop, skipper?: zk.Skipper, after?: CallableFunction[]): void {
		super.bind_(desktop, skipper, after);

		this._bindDomNode();
		if (this.frozen)
			this._cssflex = false; //force to use old flex
		if (this._cssflex && this.isChildrenFlex()) {//css flex
			this.ehdfaker!.style.display = 'none';
		} else if (this._hflex != 'min')
			this._fixHeaders();
		if (this.ehead) //sync scroll for input tab key scroll
			this.domListen_(this.ehead, 'onScroll', '_doSyncScroll');

		var ebody = this.ebody;
		if (this._nativebar && ebody) {
			this.domListen_(ebody, 'onScroll', '_doScroll');
			ebody.style.overflow = 'auto';

			if (this.efrozen && !zk.mobile)
				jq(ebody).css('overflow-x', 'hidden'); // keep non line break
		}
		zWatch.listen({ onSize: this, onResponse: this });
	}

	/** @internal */
	override unbind_(skipper?: zk.Skipper, after?: CallableFunction[], keepRod?: boolean): void {
		unlistenOnFitSize(this);
		zWatch.unlisten({ onSize: this, onResponse: this });
		if (this.ehead) //sync scroll for input tab key scroll
			this.domUnlisten_(this.ehead, 'onScroll', '_doSyncScroll');
		var ebody = this.ebody;
		if (this._nativebar && ebody) {
			this.domUnlisten_(ebody, 'onScroll', '_doScroll');
			if (this.efrozen)
				jq(ebody).css('overflow-x', 'auto');
		}
		super.unbind_(skipper, after, keepRod);
	}

	override clearCache(): void {
		super.clearCache();
		this.ebody = this.ehead = this.efoot = this.efrozen = this.ebodytbl
			= this.eheadtbl = this.efoottbl = this.ebodyrows
			= this.ehdfaker = this.ebdfaker = undefined;
	}

	/**
	 * Synchronizes the size immediately.
	 * This method is called automatically if the widget is created
	 * at the server (i.e., {@link inServer} is true).
	 * You have to invoke this method only if you create this widget
	 * at client and add or remove children from this widget.
	 * @since 5.0.8
	 */
	syncSize(): void {
		if (this.desktop) {
			this.clearCachedSize_();
			if (this._hflex == 'min') {
				zFlex.onFitSize.call(this);
			} else {
				this._fixHeaders();
			}
			zWatch.fireDown('onSize', this);
		}
	}

	// These paramters are not used. They exist for the sake of inheritance.
	onResponse(ctl?: zk.ZWatchController, opts?: Record<string, unknown>): void {
		if (this._shallSize) {
			if (this._shallClearTableWidth) {
				this._clearTableWidth();
				this._shallClearTableWidth = false;
			}
			this.syncSize();
			this._shallSize = false; // just in case
		}
	}

	/** @internal */
	_syncSize(shallClearTableWidth?: boolean): void {
		// fixed for F50-3025422.zul on ZTL
		if (this.desktop) {
			this._shallSize = true;
			this._shallClearTableWidth = !!shallClearTableWidth;
		}
	}

	/** @internal */
	_clearTableWidth(): void {
		if (this.desktop) {
			// clear table width
			var hdtbl = this.eheadtbl,
				bdtbl = this.ebodytbl,
				fttbl = this.efoottbl;
			if (hdtbl)
				hdtbl.style.width = '';
			if (bdtbl)
				bdtbl.style.width = '';
			if (fttbl)
				fttbl.style.width = '';
		}
	}

	/** @internal */
	_fixHeaders(force?: boolean): boolean { //used in HeadWidget
		if (this.head && this.ehead) {
			if (this._cssflex && this.isChildrenFlex()) return false;
			var empty = true,
				flex = false,
				shouldFix = false,
				shouldCheckEmptyWidth = !this.frozen && this.eheadtbl && this.eheadtbl.style.width,
				hdsmin = (this._hflex == 'min') || this.isSizedByContent();
			for (var i = this.heads.length; i-- > 0;) {
				var header = this.heads[i],
					emptyHeader = true,
					column = 0;
				for (var w = header.firstChild; w; w = w.nextSibling) {
					//B70-ZK-2559: when dynamic adding auxhead, there has already
					//been auxhead widget while dom element hasn't attached yet
					var childNode = this.ehdfaker!.childNodes[column++];
					if (hdsmin && (childNode && !(childNode as HTMLElement).style.width) && !w._nhflex) {
						// B50-3357475: assume header hflex min if width/hflex unspecified
						w._hflex = 'min';
						w._nhflex = -65500; // min
						w._nhflexbak = true;
					}
					if (!flex && w._nhflex)
						flex = true;
					if (w.getLabel() || w.getImage() || w.nChildren) {
						emptyHeader = false;
						empty = false;
					}
					if (shouldCheckEmptyWidth && !w.getWidth()) {
						shouldFix = true;
					}
				}

				if (header._visible) {
					//B70-ZK-2559: description as mentioned
					var n = header.$n();
					if (n)
						n.style.display = emptyHeader ? 'none' : ''; // Bug ZK-2348
				}
			}
			var old = this.ehead.style.display,
				tofix = force && flex && this.isRealVisible(); //Bug ZK-1647: no need to consider empty header for flex calculation
			this.ehead.style.display = empty ? 'none' : '';
			//onSize is not fired to empty header when loading page, so we have to simulate it here
			for (var w = this.head.firstChild; w; w = w.nextSibling) {
				if (tofix && w._nhflex)
					w.fixFlex_();
				if (w._nhflexbak) {
					delete w._hflex;
					delete w._nhflex;
					delete w._nhflexbak;
				}
			}
			return shouldFix || old != this.ehead.style.display;
		}
		return false;
	}

	/** @internal */
	_adjFlexWd(): void { //used by HeadWidget
		var head = this.head;
		if (head) {
			var hdfaker = this.ehdfaker!,
				bdfaker = this.ebdfaker!,
				hdcol = hdfaker.firstChild,
				bdcol = bdfaker.firstChild,
				ftfaker = this.eftfaker,
				// eslint-disable-next-line zk/noNull
				ftcol: ChildNode | null = null;


			if (ftfaker)
				ftcol = ftfaker.firstChild;

			//B70-ZK-2130: clean table width to prevent incorrect width
			this.eheadtbl!.style.width = '';
			this.ebodytbl!.style.width = '';

			//B70-ZK-2394: store total bdcol width
			var tblWidth = 0,
				cachedOffsetWidths: Record<string, number> = {};

			// ZK-2098: should skip if bdcol doesn't exist
			for (var w = head.firstChild; w && bdcol; w = w.nextSibling) {
				// ZK-2130: should save the header width
				// ZK-2071: use offsetWidth instead of style.width
				cachedOffsetWidths[w.uuid] = w.$n_().offsetWidth;
				bdcol = bdcol.nextSibling;
			}

			bdcol = bdfaker.firstChild;
			// ZK-4320: Do not adjust widget's width if smooth is false and frozen is scrolled
			var frozen = this.frozen;
			if (!(frozen && !frozen._smooth && frozen.getStart())) {
				for (var w = head.firstChild, wd: number | undefined; w && bdcol; w = w.nextSibling) {
					var wwd = cachedOffsetWidths[w.uuid];
					if (w.isVisible() && wwd > 0.1)
						w._origWd = jq.px0(wwd);
					// B70-ZK-2036: Do not adjust widget's width if it is not visible.
					if (w.isVisible() && (wd = w._hflexWidth) !== undefined) {
						var revisedWidth = zk(bdcol).revisedWidth(Math.round(wd)),
							revisedWidthPx = jq.px0(revisedWidth);
						//B70-ZK-2509: w.$n().offsetWidth is small when there are many columns at beginning, so save revised width if any
						w._origWd = revisedWidthPx;
						(bdcol as HTMLElement).style.width = revisedWidthPx;
						(hdcol as HTMLElement).style.width = revisedWidthPx;
						if (ftcol)
							(ftcol as HTMLElement).style.width = revisedWidthPx;

						//B70-ZK-2394: store total bdcol width
						tblWidth += revisedWidth;
					}
					bdcol = bdcol.nextSibling;
					hdcol = hdcol!.nextSibling;
					if (ftcol)
						ftcol = ftcol.nextSibling;
				}
			}

			//B70-ZK-2394: sync width from colgroup to hdtbl, bdtbl, fttbl
			var allWidths = this._isAllWidths();
			if (allWidths) {
				var hdtbl = this.eheadtbl,
					bdtbl = this.ebodytbl,
					fttbl = this.efoottbl;

				if (hdtbl) {
					var tblWidthPx = jq.px0(tblWidth);
					hdtbl.style.width = tblWidthPx;
					if (bdtbl)
						bdtbl.style.width = tblWidthPx;
					if (fttbl)
						fttbl.style.width = tblWidthPx;
				}
			}

			_adjMinWd(this);
			this._afterCalcSize();
		}
	}

	/** @internal */
	_bindDomNode(): void {
		this.ehead = this.$n('head');
		this.eheadtbl = this.$n('headtbl');
		this.ebody = this.$n('body');
		this.ebodytbl = this.$n('cave');
		this.efoot = this.$n('foot');
		this.efoottbl = this.$n('foottbl');
		this.efrozen = this.$n('frozen');

		// Grid will bind ebodyrows in Rows widget
		var erows = this.$n<HTMLTableSectionElement>('rows');
		if (this.ebody && erows)
			this.ebodyrows = erows;

		if (this.ehead) {
			this.eheadrows = this.$n('headrows');
			this.ehdfaker = this.head!.$n('hdfaker');
			this.ebdfaker = this.head!.$n('bdfaker');
			if (this.efoot)
				this.eftfaker = this.head!.$n('ftfaker');
		}
		if (this.efoot)
			this.efootrows = this.$n('footrows');
	}

	override replaceHTML(n: HTMLElement | string, desktop?: zk.Desktop, skipper?: zk.Skipper, _trim_?: boolean, _callback_?: CallableFunction[]): void { // tree outer
		var old = this._syncingbodyrows;
		this._syncingbodyrows = true;
		try {
			//bug #2995434
			//20100503, Henri: cannot use $supers('replaceHTML') since it
			//will recursive back to this function via fire('onSize'). However,
			//ZK's $supers() is simulated and when we call $supers() again
			//here, the system thought it is calling from its super class rather
			//than this class and it will be wrong. Therefore, we are forced to
			//call super class's replaceHTML directly instead.
			//Therefore, we have to specify MeshWidget as follows
			super.replaceHTML(n, desktop, skipper, _trim_, _callback_);
		} finally {
			this._syncingbodyrows = old;
		}
	}

	/** @internal */
	override replaceChildHTML_(child: zk.Widget, n: HTMLElement | string, desktop?: zk.Desktop, skipper?: zk.Skipper, _trim_?: boolean): void { // rows outer
		var old = this._syncingbodyrows;
		this._syncingbodyrows = true;
		try {
			super.replaceChildHTML_(child, n, desktop, skipper, _trim_);
		} finally {
			this._syncingbodyrows = old;
		}
	}

	fireOnRender(timeout: number): void {
		if (!this._pendOnRender) {
			this._pendOnRender = true;
			setTimeout(this.proxy(this._onRender), timeout ? timeout : 100);
		}
	}

	/** @internal */
	_doScroll(): void { //called zkmax, overriden in Listbox
		var t = zul.mesh.Scrollbar.getScrollPosV(this),
			l = zul.mesh.Scrollbar.getScrollPosH(this),
			hScrolled = l != this._currentLeft,
			scrolled = (t != this._currentTop || hScrolled),
			ebody = this.ebody!,
			ehead = this.ehead,
			efoot = this.efoot;

		// ZK-2069: fire onScroll if has scrollable property
		if (jq(this).data('scrollable')) {
			zWatch.fireDown('onScroll', this);
			zWatch.fire('_onSyncScroll', this); // ZK-4408: for Popup only
		}

		//B70-ZK-2070: if scrolled, the scrollbar need fire onScroll event.
		if (scrolled && !(this.fire('onScroll', ebody.scrollLeft).stopped) && this._nativebar)
			if (this._currentLeft != ebody.scrollLeft) {
				if (ehead)
					ehead.scrollLeft = ebody.scrollLeft;
				if (efoot)
					efoot.scrollLeft = ebody.scrollLeft;
			}

		// ZK-2046: should sync currentTop in rod mode see also Bug ZK-353
		if (scrolled /* && !this._listbox$rod && !this._grid$rod*/)
			this._currentTop = t;

		if (scrolled) // always sync for B30-1737660.zul
			this._currentLeft = l;

		if (!this.paging && !this._paginal)
			this.fireOnRender(zk.gecko ? 200 : 60);

		if (scrolled)
			this._fireOnScrollPos();

		if (this.frozen && hScrolled)
			this.frozen.syncScrollByParentBody();
	}

	/** @internal */
	_doSyncScroll(): void { //sync scroll for input tab key scroll
		var ehead = this.ehead,
			ebody = this.ebody,
			efoot = this.efoot;
		if (ehead && zk(ehead).isVisible()) {
			if (this._currentLeft != ehead.scrollLeft) {
				if (ebody)
					ebody.scrollLeft = ehead.scrollLeft;
				if (efoot)
					efoot.scrollLeft = ehead.scrollLeft;
			}
		}
	}

	/** @internal */
	_timeoutId: number | undefined;

	/** @internal */
	_fireOnScrollPos(time?: number, forceToScroll?: boolean /* overriden in zkmax */): void {
		clearTimeout(this._timeoutId);
		this._timeoutId = setTimeout(this._onScrollPos.bind(this), time! >= 0 ? time : 300);
	}

	/** @internal */
	_onScrollPos(): void {
		// Bug ZK-414
		if (this.ebody) {
			this._currentTop = zul.mesh.Scrollbar.getScrollPosV(this);
			this._currentLeft = zul.mesh.Scrollbar.getScrollPosH(this);
			this.fire('onScrollPos', {
				top: this._currentTop,
				left: this._currentLeft
			});
		}
	}

	/** @internal */
	_onRender(): boolean { //overriden in zkmax
		if (!this.$n())
			return false; // the target may not exist. B50-ZK-963

		this._pendOnRender = false;
		if (this._syncingbodyrows || zAu.processing()) { //wait if busy (it might run outer)
			this.fireOnRender(zk.gecko ? 200 : 60); //is syncing rows, try it later
			return true;
		}

		var rows = this.ebodyrows ? this.ebodyrows.rows : undefined;
		if (this.inPagingMold() && this._autopaging && rows && rows.length)
			if (_fixPageSize(this, rows))
				return false; //need to reload with new page size

		if (!this.desktop || !this._model || !rows || !rows.length)
			return false;

		//Note: we have to calculate from top to bottom because each row's
		//height might diff (due to different content)
		var items: zk.Widget[] = [],
			min = zul.mesh.Scrollbar.getScrollPosV(this),
			max = min + this.ebody!.offsetHeight;
		if (min == 0 && max == 0)
			return false; //ZK-2796: Uncessary onRender command triggered when setting tabbox's maximalHeight attribute to true
		for (var j = 0, it = this.getBodyWidgetIterator({ skipHidden: true }),
			len = rows.length, w: zul.mesh.Item | undefined; (w = it.next()) && j < len; j++) {
			if (!w._loaded) {
				//B70-ZK-2589: w and rows[j] belongs to different widget,
				//w shouldn't depend on rows[j], origin -> row = rows[j];
				var row = w.$n();
				if (row == null) continue;
				var $row = zk(row),
					top = $row.offsetTop();

				if (top + $row.offsetHeight() < min) continue;
				if (top > max) break; //Bug 1822517
				items.push(w);
			}
		}
		if (items.length) {
			this.fire('onRender', { items: items }, { implicit: true });
		}
		return false;
	}

	override onSize(): void {
		var devicePixelRatio = zUtl.getDevicePixelRatio();
		if (this.isRealVisible()) { // sometimes the caller is not zWatch
			var n = this.$n_();
			if (n._lastsz && n._lastsz.height == n.offsetHeight
				&& n._lastsz.width == n.offsetWidth
				&& this._lastDevicePixelRatio == devicePixelRatio) {
				this.fireOnRender(155); // force to render while using live grouping
				return; // unchanged
			}
			this._lastDevicePixelRatio = devicePixelRatio;
			this._calcSize();// Bug #1813722
			this.fireOnRender(155);

			if (this._nativebar) { // Bug ZK-355: keep scrollbar position
				var ebody = this.ebody!;
				// ZK-3647: if Listbox enable ROD and need to init pad sizes, it will adjust scrollTop when _initPadSizes().
				if (ebody.scrollHeight >= this._currentTop && !this._adjustScrollTopLater)
					ebody.scrollTop = this._currentTop;

				if (ebody.scrollWidth >= this._currentLeft) {
					ebody.scrollLeft = this._currentLeft;
					if (this.ehead)
						this.ehead.scrollLeft = this._currentLeft;
					if (this.efoot)
						this.efoot.scrollLeft = this._currentLeft;
				}
			}
			this._shallSize = false;
			if (this._keepScroll) {
				this._scrollToIndex(this._targetIndex!);
			}
		}
	}

	/** @internal */
	_vflexSize(): number {
		var n = this.$n(),
			pgHgh = 0;
		if (this.paging) {
			var pgit = this.$n('pgit'),
				pgib = this.$n('pgib');
			if (pgit) pgHgh += pgit.offsetHeight;
			if (pgib) pgHgh += pgib.offsetHeight;
		}
		// Bug #1815882 and Bug #1835369
		var hgh = zk(n).contentHeight()
			- (this.ehead ? this.ehead.offsetHeight : 0)
			- (this.efoot ? this.efoot.offsetHeight : 0)
			- pgHgh;
		return this.frozen && this._nativebar ?
			hgh - this.efrozen!.offsetHeight : hgh;
	}

	/** @internal */
	override setFlexSize_(flexSize: zk.FlexSize, isFlexMin?: boolean): void {
		if (this._cssflex && this.parent!.getFlexContainer_() != null && !isFlexMin)
			return;
		var n = this.$n_(),
			head = this.$n('head');
		if (flexSize.height !== undefined) {
			if (flexSize.height == 'auto') {
				n.style.height = '';
				if (head)
					head.style.height = '';
			} else {
				return super.setFlexSize_(flexSize, isFlexMin);
			}
		}
		if (flexSize.width !== undefined) {
			if (flexSize.width == 'auto') {
				if (this._hflex != 'min')
					n.style.width = '';
				if (head)
					head.style.width = '';
			} else {
				return super.setFlexSize_(flexSize, isFlexMin);
			}
		}
	}

	/* set the height. */
	/** @internal */
	_setHgh(hgh?: string): void {
		var n = this.$n_(),
			ebody = this.ebody!,
			ebodyStyle = ebody.style;
		if (this.isVflex() || (hgh && hgh != 'auto' && !hgh.includes('%'))) {
			if (zk.webkit && ebodyStyle.height == jq.px(this._vflexSize()))
				return; // Bug ZK-417, ignore to set the same size
			ebodyStyle.height = ''; //allow browser adjusting to default size
			var h = this._vflexSize();
			if (h < 0)
				h = 0;
			if (this._vflex != 'min')
				ebodyStyle.height = `${h}px`;
		} else {
			//Bug 1556099
			ebodyStyle.height = '';
			n.style.height = hgh!;
		}
	}

	/** @internal */
	_ignoreHghExt(): boolean {
		return false;
	}

	/** Calculates the size. @internal */
	_calcSize(): void {
		this._beforeCalcSize();
		//Bug 1553937: wrong sibling location
		//Otherwise,
		//IE: element's width will be extended to fit body
		//note: we don't solve this bug for paging yet
		var n = this.$n_(),
			sizedByContent = this.isSizedByContent(),
			ehead = this.ehead,
			ebodyrows = this.ebodyrows,
			efoot = this.efoot,
			efootrows = this.efootrows;

		if (ehead) {
			if (sizedByContent && ebodyrows)
				this._adjHeadWd();
		} else if (efoot) {
			if (efootrows && ebodyrows)
				_cpCellWd(this);
		}

		//check if need to span width
		this._adjSpanWd();
		// no header case
		_fixBodyMinWd(this, true);

		// B50-ZK-543, B50-ZK-773
		// should re-calculate height because
		// the string height maybe changed after width changed.
		if (sizedByContent
			&& this.getRows && this.getRows() > 1
			&& (typeof this._calcHgh == 'function')
			&& this.ebody!.style.height) { // check only if height exists for F50-3000339.zul
			this._calcHgh(); // recalculate height again ZK-796
		}

		n._lastsz = { height: n.offsetHeight, width: n.offsetWidth }; // cache for the dirty resizing.

		this._afterCalcSize();
	}

	/** @internal */
	_beforeCalcSize(): void {
		var ebody = this.ebody!;
		if (!this._nativebar && (ebody.scrollLeft || ebody.scrollTop)) {
			// ZK-2046: Keep ebody scroll position before calculated size, _setHgh would reset it to 0.
			this._ebodyScrollPos = { l: ebody.scrollLeft, t: ebody.scrollTop };
		}
		this._calcHgh();
	}

	/** @internal */
	_afterCalcSize(): void {
		// Fix B96-ZK-5247
		if (!this.isRealVisible()) return;

		var isCSSFlex = this._cssflex && this.isChildrenFlex();
		if (this._ebodyScrollPos) {
			// ZK-2046: Restore ebody scroll position after calculated size.
			this.ebody!.scrollLeft = this._ebodyScrollPos.l;
			this.ebody!.scrollTop = this._ebodyScrollPos.t;
			this._ebodyScrollPos = undefined;
		}
		// Set style width to table to avoid colgroup width not working
		// because of width attribute (width="100%") on table
		var allWidths = this._isAllWidths(),
			head = this.head,
			ebody = this.ebody,
			ebodyrows = this.ebodyrows,
			hdfakerbar = head ? head.$n('hdfaker-bar') : undefined,
			hdbar = head ? jq(this).find('.' + head.$s('bar'))[0] : undefined,
			ftfakerbar = this.eftfaker ? this.head!.$n('ftfaker-bar') : undefined,
			scrollbarWidth = jq.scrollbarWidth(),
			hasVScroll = zk(ebody).hasVScroll(),
			hasHScroll = zk(ebody).hasHScroll();

		if (hasVScroll) {
			if (hdfakerbar) {
				if (isCSSFlex && hdbar)
					hdbar.style.flex = `0 1 ${scrollbarWidth}px`;
				else
					hdfakerbar.style.width = `${scrollbarWidth}px`;
			}
			if (ftfakerbar)
				ftfakerbar.style.width = `${scrollbarWidth}px`;
		} else {
			var zero = MeshWidget.WIDTH0;
			//refix B70-ZK-2114: remove hdfakerbar when there is no native scrollbar
			if (hdfakerbar) {
				if (isCSSFlex && hdbar)
					hdbar.style.flex = '0 1 ' + zero + 'px';
				else
					hdfakerbar.style.width = zero;
			}
			if (ftfakerbar)
				ftfakerbar.style.width = zero;
		}
		if (hasHScroll && ebodyrows && this._vflex == 'min') {
			var ehead = this.ehead,
				efoot = this.efoot,
				hgh = jq(ebodyrows).height()!;
			hgh += scrollbarWidth;
			ebody!.style.height = jq.px0(hgh);
			if (ehead) {
				hgh += ehead.offsetHeight;
			}
			if (efoot) {
				hgh += efoot.offsetHeight;
			}
			this.$n_().style.height = jq.px0(hgh);
		}
		if (isCSSFlex) {
			//update
			head!.afterChildrenFlex_();
			return;
		}
		if (allWidths) {
			var hdtbl = this.eheadtbl,
				bdtbl = this.ebodytbl,
				fttbl = this.efoottbl;

			if (hdtbl) {
				var wd = 0;
				for (var w = this.ehdfaker!.firstChild; w; w = w.nextSibling) {
					const n = w as HTMLElement;
					if (n.style.display != 'none' && !n.id.endsWith('hdfaker-bar')) // B70-ZK-2307 and B70-ZK-2358
						wd += zk.parseInt(n.style.width);
				}
				if (wd > 0) { //ZK-2772, ZK-2903: only when hdfaker has width, set back to table
					//ZK-3938: only adjust width in Chrome (ZK-4219: and safari), but zk.chrome returns true in Edge, we need to check !zk.edge
					hdtbl.style.width = (hdfakerbar && hasVScroll && (zk.chrome || zk.safari) && !zk.edge_legacy) ? `${wd + scrollbarWidth}px` : `${wd}px`;
					if (bdtbl)
						bdtbl.style.width = `${wd}px`;
					if (fttbl)
						fttbl.style.width = `${wd}px`;
				} else {
					var hideTable = false;
					for (var header = this.head!.firstChild; header; header = header.nextSibling) {
						if (header.isVisible()) {
							hideTable = false;
							break;
						}
						hideTable = true;
					}
					if (hideTable) {
						hdtbl.style.visibility = 'hidden';
						if (bdtbl)
							bdtbl.style.visibility = 'hidden';
						if (fttbl)
							fttbl.style.visibility = 'hidden';
					}
				}
			}
		} else if (this.frozen) {
			//B70-ZK-2468: should sync ebody width with ebodytbl width
			var ehead = this.ehead,
				hdtbl = this.eheadtbl,
				ebody = this.ebody,
				bdtbl = this.ebodytbl,
				efoot = this.efoot,
				fttbl = this.efoottbl;
			if (ehead && hdtbl)
				hdtbl.style.width = jq.px0(ehead.clientWidth);
			if (ebody && bdtbl)
				bdtbl.style.width = jq.px0(ebody.clientWidth);
			if (efoot && fttbl)
				fttbl.style.width = jq.px0(efoot.clientWidth);

			if (!this.frozen._smooth)
				this._syncFaker();
		}
	}

	/** @internal */
	_syncFaker(): void {
		var head = this.head;
		if (!head || !this.desktop)
			return;

		var totalCols = head.nChildren,
			width0 = zul.mesh.MeshWidget.WIDTH0,
			newTotalWidth = this.ebodytbl!.clientWidth,
			oldTotalWidth = 0,
			epsilon = 1;
		for (var i = 0, header = head.firstChild!, hdcol = this.ehdfaker!.firstChild; i < totalCols;
			header = header.nextSibling!, hdcol = hdcol!.nextSibling, i++) {
			var fakerStyleWidth = (hdcol as HTMLElement).style.width;
			if (fakerStyleWidth == '')
				return;

			var isHidden = fakerStyleWidth == width0,
				width = isHidden ? parseFloat(header._origWd!) : parseFloat(fakerStyleWidth);
			if (header.getWidth() || header.getHflex())
				newTotalWidth -= width;
			else
				oldTotalWidth += width;
		}

		if (Math.abs(newTotalWidth - oldTotalWidth) < epsilon)
			return;

		for (var i = 0, header = head.firstChild!, hdcol = this.ehdfaker!.firstChild; i < totalCols;
			header = header.nextSibling!, hdcol = hdcol!.nextSibling, i++) {
			if (header.getWidth() || header.getHflex())
				continue;

			var fakerStyleWidth = (hdcol as HTMLElement).style.width,
				multiplier = newTotalWidth / oldTotalWidth,
				isHidden = fakerStyleWidth == width0;
			if (isHidden)
				header._origWd = jq.px0(parseFloat(header._origWd!) * multiplier);
			else {
				var newStyleWidth = jq.px0(parseFloat(fakerStyleWidth) * multiplier),
					bdfaker = header.$n('bdfaker'),
					ftfaker = header.$n('ftfaker');
				(hdcol as HTMLElement).style.width = newStyleWidth;
				if (bdfaker)
					bdfaker.style.width = newStyleWidth;
				if (ftfaker)
					ftfaker.style.width = newStyleWidth;
			}
		}
	}

	//return if all widths of columns are fixed (directly or indirectly)
	/** @internal */
	_isAllWidths(): boolean {
		// ZK-2157: should skip if the mesh has no children
		if (this.isSizedByContent() && this.ebodyrows && this.ebodyrows.firstChild)
			return true;
		if (!this.head)
			return false;
		for (var w = this.head.firstChild; w; w = w.nextSibling) {
			if ((w._width == undefined || w._width.indexOf('px') <= 0)
				&& (w._hflex != 'min' || w._hflexsz === undefined)
				&& w.isVisible()) {
				return false;
			}
		}
		return true;
	}

	/** @internal */
	domFaker_(out: string[], fakeId: string): void { //used by redraw
		var head = this.head!;
		out.push('<colgroup id="', /*safe*/ head.uuid, /*safe*/ fakeId, '">');
		for (var w = head.firstChild; w; w = w.nextSibling) {
			var wd = MeshWidget._getWidth(w, w._hflexWidth ? `${w._hflexWidth}px` : w.getWidth()),
				visibility = w.isVisible() ? '' : 'visibility: collapse;';
			// B70-ZK-2036: Style width should end with 'px'.
			wd = wd != null ? 'width: ' + wd + ';' : '';
			out.push('<col id="', /*safe*/ w.uuid, /*safe*/ fakeId, '" style="', /*safe*/ wd, /*safe*/ visibility, '"></col>');
		}
		if (fakeId.indexOf('hd') > 0 || fakeId.indexOf('ft') > 0)
			out.push('<col id="', /*safe*/ head.uuid, /*safe*/ fakeId, '-bar" style="width: 0px" ></col>');
		out.push('</colgroup>');
	}


	/** @internal */
	override onChildAdded_(child: zk.Widget): void {
		super.onChildAdded_(child);

		if (child instanceof this.getHeadWidgetClass()) {
			this.head = child;
			this._minWd = undefined;
			// TODO: remove the type assertion below. Should be unnecessary, but tsc infers it
			// to be `never` which is not wrong.
		} else if (!(child instanceof zul.mesh.Auxhead))
			return;

		var nsib = child.nextSibling;
		if (nsib)
			for (var hds = this.heads, j = 0, len = hds.length; j < len; ++j)
				if (hds[j] == nsib) {
					hds.splice(j, 0, child);
					return; //done
				}
		this.heads.push(child);
	}

	/** @internal */
	override onChildRemoved_(child: zk.Widget): void {
		super.onChildRemoved_(child);

		if (child == this.head) { // If true, child is guaranteed to be a HeadWidget
			this._minWd = this.head = undefined;
			this.heads.$remove(child as zul.mesh.HeadWidget);
		} else if (child instanceof zul.mesh.Auxhead)
			this.heads.$remove(child);
		else if (child instanceof zul.mesh.Frozen)
			this.efrozen = undefined;
	}

	//bug# 3022669: listbox hflex="min" sizedByContent="true" not work
	/** @internal */
	override beforeMinFlex_(orient: zk.FlexOrient): number | undefined {
		if (this._hflexsz === undefined && orient == 'w' && this._width === undefined) {
			if (this.isSizedByContent())
				this._calcSize();
			if (this.head) {
				this._fixHeaders(true);/* B50-3315594.zul */
				for (var w = this.head.firstChild; w; w = w.nextSibling)
					if (w._hflex == 'min' && w._hflexsz === undefined) //header hflex="min" not done yet!
						return undefined;
			}
			_fixBodyMinWd(this); // sized by content without header
			return _getMinWd(this); //grid.invalidate() with hflex="min" must maintain the width
		}
		return undefined;
	}

	// fixed for B50-3315594.zul
	/** @internal */
	override beforeParentMinFlex_(orient: zk.FlexOrient): void {
		if (orient == 'w') {
			if (this.isSizedByContent())
				this._calcSize();
			if (this.head)
				this._fixHeaders();
		} else
			this._calcSize();
	}

	/** @internal */
	override clearCachedSize_(): void {
		super.clearCachedSize_();
		this._clearCachedSize();

		var tr: HTMLTableRowElement | undefined;
		if (!this.ebdfaker && (tr = _getSigRow(this))) { //empty head case
			for (var cells = tr.cells, i = cells.length; i--;)
				cells[i].style.width = '';
		}
		var head = this.getHeadWidget();
		if (head) {
			for (var w = head.firstChild; w; w = w.nextSibling)
				delete w._hflexsz;
		}
	}

	/** @internal */
	_clearCachedSize(): void {
		const n = this.$n();
		if (n)
			n._lastsz = this._minWd = undefined;
	}

	/** @internal */
	_calcMinWds(): MeshWidth { //used in HeaderWidgets
		if (!this._minWd)
			this._minWd = _calcMinWd(this);
		return this._minWd;
	}

	/** @internal */
	_adjSpanWd(): void { //used in HeadWidgets
		if (!this._isAllWidths() || !this.isSpan())
			return;
		var hdfaker = this.ehdfaker,
			bdfaker = this.ebdfaker,
			ftfaker = this.eftfaker;
		if (!hdfaker || !bdfaker)
			return;

		var head = this.head!.$n();
		if (!head)
			return;
		this._calcMinWds();
		var wd: number,
			wds: number[] = [],
			width = 0,
			hdcol = hdfaker.firstChild,
			bdcol = bdfaker.firstChild,
			_minwds = this._minWd!.wds,
			hdlen = this.head!.nChildren;

		for (var temphdcol = hdcol, w = this.head!.firstChild, i = 0; w; w = w.nextSibling, i++) {
			if (zk(temphdcol).isVisible(true)) {
				var wdh = w._width;

				if (w._hflex == 'min')
					wd = wds[i] = _minwds[i];
				else if (wdh?.endsWith('px'))
					wd = wds[i] = zk.parseInt(wdh);
				else
					wd = wds[i] = _minwds[i];

				width += wd;
			}
			temphdcol = temphdcol!.nextSibling;
		}

		var ftcol = ftfaker?.firstChild,
			total = this.ebody!.clientWidth,
			extSum = total - width,
			count = total,
			visj = -1,
			tblWidth = 0; //refix B70-ZK-2394: should sync colgroup width with table width

		//span to all columns
		if (this._nspan! < 0) {


			var hasFrozenScrolled = this.frozen?.getStart();
			for (var i = 0; hdcol && i < hdlen; hdcol = hdcol.nextSibling, i++) {
				// ZK-2222: should check visibility
				if (!zk(hdcol).isVisible(true)) {
					bdcol = bdcol!.nextSibling;
					if (ftcol)
						ftcol = ftcol.nextSibling;
					continue;
				} else {

					// for bug ZK-2772, we don't span it when frozen column has scrolled.
					// Instead, we use its faker width.
					if (hasFrozenScrolled) {
						if (extSum <= 0) {
							wds[i] = wd = wds[i];
						} else {
							if ((hdcol as HTMLElement).style.width) {
								wds[i] = wd = zk.parseInt((hdcol as HTMLElement).style.width);
							} else {
								wds[i] = wd = Math.round(((wds[i] * total / width) + 0.5) || 0);
							}
						}
					} else {
						wds[i] = wd = extSum <= 0 ? wds[i] : Math.round(((wds[i] * total / width) + 0.5) || 0);
					}
					var stylew = jq.px0(wd);
					count -= wd;
					visj = i;

					(hdcol as HTMLElement).style.width = stylew;
					(bdcol as HTMLElement).style.width = stylew;
					tblWidth += wd; //refix B70-ZK-2394: store each col's width
					bdcol = bdcol!.nextSibling;

					if (ftcol) {
						(ftcol as HTMLElement).style.width = stylew;
						ftcol = ftcol.nextSibling;
					}
				}
			}
			//compensate calc error but excluding scrolled frozen column
			if (extSum > 0 && count != 0 && visj >= 0) {
				tblWidth -= wd!; //refix B70-ZK-2394: subtract the last wd (visj is the last)
				wd = wds[visj] + count;
				var stylew = jq.px0(wd);

				tblWidth += wd; //refix B70-ZK-2394: and add new wd

				if (!hasFrozenScrolled) {
					(bdfaker.childNodes[visj] as HTMLElement).style.width = stylew;
					(hdfaker.childNodes[visj] as HTMLElement).style.width = stylew;

					if (ftfaker)
						(ftfaker.childNodes[visj] as HTMLElement).style.width = stylew;
				}
			}
		} else { //feature#3184415: span to a specific column
			visj = this._nspan! - 1;
			for (var i = 0; hdcol && i < hdlen; hdcol = hdcol.nextSibling, i++) {
				if (!zk(hdcol).isVisible(true)) {
					bdcol = bdcol!.nextSibling;
					if (ftcol)
						ftcol = ftcol.nextSibling;
					continue;
				} else {
					wd = visj == i && extSum > 0 ? (wds[visj] + extSum) : wds[i];
					var stylew = jq.px0(wd);
					(hdcol as HTMLElement).style.width = stylew;
					(bdcol as HTMLElement).style.width = stylew;
					tblWidth += wd; //refix B70-ZK-2394: store each col's width
					bdcol = bdcol!.nextSibling;
					if (ftcol) {
						(ftcol as HTMLElement).style.width = stylew;
						ftcol = ftcol.nextSibling;
					}
				}
			}
		}

		//refix B70-ZK-2394: sync colgroup width with (head, body, foot)table width
		var allWidths = this._isAllWidths();
		if (allWidths) {
			var hdtbl = this.eheadtbl,
				bdtbl = this.ebodytbl,
				fttbl = this.efoottbl;

			if (hdtbl) {
				hdtbl.style.width = `${tblWidth}px`;
				if (bdtbl)
					bdtbl.style.width = `${tblWidth}px`;
				if (fttbl)
					fttbl.style.width = `${tblWidth}px`;
			}
		}

		//bug 3188738: Opera only. Grid/Listbox/Tree span="x" not working
		if (zk.opera)
			zk(this.$n()).redoCSS();
	}

	/** @internal */
	_adjHeadWd(): void {
		var hdfaker = this.ehdfaker,
			bdfaker = this.ebdfaker,
			ftfaker = this.eftfaker;

		if (!hdfaker || !bdfaker || !this.getBodyWidgetIterator().hasNext())
			return;

		var hdtable = this.eheadtbl!,
			head = this.head!.$n();

		if (!head)
			return;

		// Bug #1886788 the size of these table must be specified a fixed size.
		var ebody = this.ebody!,
			bdtable = this.ebodytbl!,
			bdwd = ebody.offsetWidth,
			total = Math.max(hdtable.offsetWidth, bdtable.offsetWidth),
			tblwd = Math.min(bdwd, bdtable.offsetWidth);

		if (total == bdwd && bdwd > tblwd && bdwd - tblwd < 20)
			total = tblwd;

		var minWd = this._calcMinWds(),
			wds = minWd.wds,
			width = minWd.width,
			hdcol = hdfaker.firstChild,
			bdcol = bdfaker.firstChild,
			ftcol = ftfaker ? ftfaker.firstChild : undefined,
			hwgt = this.head!.firstChild,
			notVisibleWidth = MeshWidget.WIDTH0;

		for (var i = 0; hwgt; hwgt = hwgt.nextSibling, i++) {
			if (hwgt.isVisible()) {
				// sizedByContent shall not override column width
				if (hwgt._width || wds[i] == 0) {
					if (wds[i] == 0) {
						(hdcol as HTMLElement).style.width = notVisibleWidth;
						(bdcol as HTMLElement).style.width = notVisibleWidth;
						if (ftcol)
							(ftcol as HTMLElement).style.width = notVisibleWidth;
					}
					hdcol = hdcol!.nextSibling;
					bdcol = bdcol!.nextSibling;
					if (ftcol)
						ftcol = ftcol.nextSibling;
				} else if (!this.frozen || !this.frozen.getStart() || (hdcol as HTMLElement).style.width === '') {
					var wd = jq.px(wds[i]);
					(hdcol as HTMLElement).style.width = (bdcol as HTMLElement).style.width = wd;
					hdcol = hdcol!.nextSibling;
					bdcol = bdcol!.nextSibling;
					if (ftcol) {
						(ftcol as HTMLElement).style.width = wd;
						ftcol = ftcol.nextSibling;
					}
				}
			} else {
				(hdcol as HTMLElement).style.width = notVisibleWidth;
				(bdcol as HTMLElement).style.width = notVisibleWidth;
				hdcol = hdcol!.nextSibling;
				bdcol = bdcol!.nextSibling;
				if (ftcol) {
					(ftcol as HTMLElement).style.width = notVisibleWidth;
					ftcol = ftcol.nextSibling;
				}
			}
		}

		hdtable.style.width = jq.px(width);
		bdtable.style.width = jq.px(width);
		if (ftfaker)
			this.efoottbl!.style.width = jq.px(width);

		_adjMinWd(this);
	}

	/** @internal */
	_getFirstRowCells(tbody: HTMLTableSectionElement | undefined): HTMLCollectionOf<HTMLTableCellElement> | undefined {
		var rowsLength: number,
			tbodyrows: HTMLCollectionOf<HTMLTableRowElement>;
		if (tbody && (tbodyrows = tbody.rows) && (rowsLength = tbodyrows.length)) {
			var cells = tbodyrows[0].cells,
				length = cells.length,
				ncols = 0;
			//ZK-2752: find the first visible row
			//keep the same behavior even there isn't any row visible to avoid other side effects
			if (tbody.offsetHeight > 0) {
				for (var i = 0; i < rowsLength; i++) {
					if (jq(tbodyrows[i]).css('display') != 'none') {
						cells = tbodyrows[i].cells;
						length = cells.length;
						break;
					}
				}
			}
			for (var i = 0; i < length; i++) {
				var span = cells[i].colSpan;
				ncols += span != 1 ? span : 1;
			}
			if (ncols == length)
				return cells;
			else {
				var outHTML: string | undefined = '';
				outHTML += '<tr id="' + /*safe*/ tbody.id + '-fakeRow" style="visibility:hidden;height:0">';
				for (var i = 0; i < ncols; i++)
					outHTML += '<td></td>';
				outHTML += '</tr>';
				jq(tbodyrows[0]).before(/*safe*/ outHTML);
				outHTML = undefined;
				return tbodyrows[0].cells;
			}
		}
	}

	/** @internal */
	_deleteFakeRow(tbody: HTMLTableSectionElement | undefined): void {
		if (tbody)
			jq('#' + tbody.id + '-fakeRow').remove();
	} // for Grid.js and Listbox.js

	/** @internal */
	refreshBar_(showBar?: boolean, scrollToTop?: boolean): void {
		var bar = this._scrollbar;
		if (bar) {
			// ZK-355: Keep scroll position before sync scrollbar size
			var currentLeft = this._currentLeft,
				currentTop = this._currentTop;
			bar.syncSize(showBar || this._shallShowScrollbar);
			delete this._shallShowScrollbar; // use undefined rather false
			if (scrollToTop)
				bar.scrollTo(0, 0);
			else
				bar.scrollTo(currentLeft, currentTop);
			//sync frozen
			var frozen = this.frozen,
				start: number;
			if (frozen && (start = frozen._start) != 0) {
				frozen._doScrollNow(start);
				bar.setBarPosition(start);
			}
			this._afterCalcSize(); // for ZK-2370, we need to check the faker-bar again
		}
	}

	onFitSize(): void {
		// B50-ZK-598: when having rows, height needs to be determined when onFitSize
		if (this._rows)
			this._calcHgh();
	}

	/** @internal */
	_calcHgh(): void {
		var rows: ArrayLike<HTMLTableRowElement> = this.ebodyrows ? this.ebodyrows.rows : [],
			n = this.$n_(),
			hgh: string | number = n.style.height,
			isHgh = hgh && hgh != 'auto' && !hgh.includes('%');
		if (isHgh) {
			hgh = zk.parseInt(hgh) - zk(n).padBorderHeight();
			if (hgh) {
				hgh -= this._headHgh(0);
				if (hgh < 0) hgh = 0;
				var sz = 0;
				l_out:
				for (var h, j = 0, rl = rows.length; j < rl; ++sz, ++j) {
					//next visible row
					var r: HTMLTableRowElement;
					for (; ; ++j) {//no need to check length again
						if (j >= rl) break l_out;
						r = rows[j];
						if (zk(r).isVisible()) break;
					}

					var $r = zk(r);
					h = $r.offsetTop() + $r.offsetHeight();
					if (h >= hgh) {
						if (h > hgh + 2) ++sz; //experimental
						break;
					}
				}
				sz = Math.ceil(sz && h ? (hgh * sz) / h : hgh / this._headHgh(20));
				this._setOrGetVisibleRows(sz);
				hgh -= (this.efoot ? this.efoot.offsetHeight : 0);
				//bug# 3036398: frozen scrollbar disappear when listbox with vflex="1"
				hgh -= (this.efrozen && this._nativebar ? this.efrozen.offsetHeight : 0);
				this.ebody!.style.height = `${Math.max(hgh, 0)}px`;
				return; //done
			}
		}

		var nVisiRows = 0,
			nRows = this.getRows(),
			lastVisiRow: HTMLTableRowElement | undefined,
			firstVisiRow: HTMLTableRowElement | undefined,
			midVisiRow: HTMLTableRowElement | undefined;
		for (var j = 0, rl = rows.length; j < rl; ++j) { //tree might collapse some items
			var r = rows[j];
			if (zk(r).isVisible()) {
				++nVisiRows;
				if (!firstVisiRow)
					firstVisiRow = r;

				if (nRows === nVisiRows) {
					midVisiRow = r;
					break;
					//nVisiRows and lastVisiRow useful only if nRows is larger,
					//so ok to break here
				}
				lastVisiRow = r;
			}
		}

		hgh = 0;
		var diff = 2;/*experiment*/
		if (!nRows) {
			if (this.isVflex()) {
				// Since ie6 support was dropped, the argument of _vflexSize is not used.
				// Some time later, the argument of _vflexSize is dropped from the signature,
				// but `this._vflexSize(n.style.height)` wasn't cleaned up until now.
				hgh = this._vflexSize();

				if (hgh < 25) hgh = 25;

				var rowhgh = firstVisiRow ? zk(firstVisiRow).offsetHeight() : undefined;
				if (!rowhgh)
					rowhgh = this._headHgh(20);

				nRows = Math.round((hgh - diff) / rowhgh);
			}
			this._setOrGetVisibleRows(nRows);
		}

		if (nRows) {
			if (!hgh) {
				if (!nVisiRows) {
					hgh = this._headHgh(20, true) * nRows;
				} else if (nRows <= nVisiRows) {
					var $midVisiRow = zk(midVisiRow);
					hgh = $midVisiRow.offsetTop() + $midVisiRow.offsetHeight();
				} else {
					var $lastVisiRow = zk(lastVisiRow);
					hgh = $lastVisiRow.offsetTop() + $lastVisiRow.offsetHeight();
					hgh = Math.ceil((nRows * hgh) / nVisiRows);
				}
			}
			this.ebody!.style.height = `${hgh}px`;
		} else {
			this.ebody!.style.height = '';
			var focusEL = this.$n('a');
			if ((this.getPagingChild()) && focusEL)
				focusEL.style.top = '0px'; // Bug ZK-1715: focus has no chance to sync if don't select item after changing page.
		}
	}

	/**
	 * @returns the real # of rows (aka., real size).
	 * @internal
	 */
	_setOrGetVisibleRows(v?: number): number | undefined {
		if ('number' == typeof v) {
			this._visiRows = v;
		} else
			return this.getRows() || this._visiRows || 0;
	}

	/* Height of the head row. If no header, defval is returned. */
	/** @internal */
	_headHgh(defVal: number, isExcludeAuxhead?: boolean): number {
		var headWidget = this.getHeadWidget(), //Bug ZK-1297: get head height exclude auxhead
			head = this.ehead,
			hgh = isExcludeAuxhead ? (headWidget ? headWidget.$n_().offsetHeight : 0) : (head ? head.offsetHeight : 0);
		if (this.paging) {
			var pgit = this.$n('pgit'),
				pgib = this.$n('pgib');
			if (pgit) hgh += pgit.offsetHeight;
			if (pgib) hgh += pgib.offsetHeight;
		}
		return hgh ? hgh : defVal;
	}

	/**
	 * Scroll to the specified item by the given index, used by Grid and Listbox,
	 * this function could be invoked by server-side or client-side,
	 * when invoked by client-side, scrollRatio will be undefined.
	 * @param index - the index of item
	 * @param scrollRatio - the scroll ratio
	 * @internal
	 */
	_scrollToIndex(index: number, scrollRatio?: number): void {
		// NOTE: _scrollToIndex will only be called by Grid and Listbox, and both of them
		// implements _getFirstItemIndex and _getLastItemIndex.

		// @ts-expect-error: calling `_getFirstItemIndex` is safe in `_scrollToIndex`
		// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
		var firstItemIndex = this._getFirstItemIndex(),
			// @ts-expect-error: calling `_getLastItemIndex` is safe in `_scrollToIndex`
			// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
			lastItemIndex = this._getLastItemIndex(),
			body = this.ebody!;
		this._targetIndex = index;
		this._keepScroll = true;

		if (index >= firstItemIndex && index <= lastItemIndex) {
			var itemIterator = this.getBodyWidgetIterator();
			while (itemIterator.hasNext()) {
				var item = itemIterator.next()!;
				if (item._index == index) {
					item.$n_().scrollIntoView(true);
					this._keepScroll = false;
					this._topBoundary = this._bottomBoundary = undefined;
					return;
				}
			}
		} else if (scrollRatio != undefined) {
			body.scrollTop = body.scrollHeight * scrollRatio;
		} else if (index < firstItemIndex) {
			this._bottomBoundary = body.scrollTop;
			this._topBoundary = this._topBoundary == undefined ? 0 : this._topBoundary;
			body.scrollTop -= (body.scrollTop - this._topBoundary) / 2;
		} else if (index > lastItemIndex) {
			this._topBoundary = body.scrollTop;
			this._bottomBoundary = this._bottomBoundary == undefined ? body.scrollHeight : this._bottomBoundary;
			body.scrollTop += (this._bottomBoundary - body.scrollTop) / 2;
		}
	}

	/** @internal */
	override getContentEdgeHeight_(height: number): number {
		var height = super.getContentEdgeHeight_(height),
			efoot = this.efoot;
		if (efoot) {
			var zkefoot = zk(efoot);
			height += zkefoot.padBorderHeight() + zkefoot.sumStyles('tb', jq.margins);
		}
		return height;
	}

	/** @internal */
	override afterChildrenMinFlex_(o: zk.FlexOrient): void {
		var n = this.$n_();
		if (o == 'h') {
			n.style.height = jq.px0(Math.ceil(this._vflexsz!));
		}
	}

	//css flex
	isChildrenFlex(): boolean {
		if (this.head && this.ehead) {
			for (var i = this.heads.length; i-- > 0;) {
				var header = this.heads[i];
				for (var w = header.firstChild; w; w = w.nextSibling) {
					if (w._hflex && w._hflex != 'min') {
						return true;
					}
				}
			}
		}
		return false;
	}

	/** @internal */
	waitForRendered_(): Promise<void> {
		return new Promise(resolve => {
			const callback = (): void => {
				if (!this._pendOnRender) return resolve();
				setTimeout(callback);
			};
			callback();
		});
	}

	// ZK-5028 for Treecols, Listhead, and Columns
	/** @internal */
	override shallFireSizedLaterWhenAddChd_(): boolean {
		if (this.inRerendering_()) {
			zWatch.listen({
				onResponse: [this, this._fixFireSizedLaterWhenAddChd]
			});
			return true;
		}
		return false;
	}

	// ZK-5028 for Treecols, Listhead, and Columns
	/** @internal */
	_fixFireSizedLaterWhenAddChd(): void {
		zUtl.fireSized(this);
		zWatch.unlisten({
			onResponse: [this, this._fixFireSizedLaterWhenAddChd]
		});
	}

	static readonly WIDTH0 = zk.webkit ? '0.001px' : '0px';

	/** @internal */
	static _getWidth(wgt: zk.Widget, width: string | undefined): string | undefined {
		if (wgt.isVisible())
			return width;
		return MeshWidget.WIDTH0;
	}
}

/** @class zul.mesh.Scrollbar
 * @import zk.Widget
 * The extra Scrollbar for the MeshWidget.
 * @since 6.5.0
 */
export var Scrollbar = {
	/**
	 * Initialize the scrollbar of MeshWidget.
	 * @param wgt - a widget
	 */
	init(wgt: zul.mesh.MeshWidget): zul.Scrollbar | undefined /* zkmax-override could return undefined */ {
		var embed = jq(wgt.$n_()).data('embedscrollbar') !== false, // change default value to true since 7.0.2
			frozen = wgt.frozen,
			startPositionX = 0;

		if (frozen) {
			var columns = frozen.getColumns()!;
			if (wgt.eheadtbl) {
				var cells = wgt._getFirstRowCells(wgt.eheadrows);
				if (cells) {
					for (var i = 0; i < columns; i++)
						startPositionX += cells[i].offsetWidth;
				}
				wgt._deleteFakeRow(wgt.eheadrows);
			}
		}
		return new zul.Scrollbar(wgt.ebody, wgt.ebodytbl, {
			embed: embed,
			startPositionX: startPositionX,
			onSyncPosition: function () {
				if (!this.frozen) {
					var pos = this.getCurrentPosition(),
						head = wgt.ehead,
						foot = wgt.efoot;
					if (pos && this.hasHScroll()) {
						if (head)
							head.scrollLeft = pos.x;
						if (foot)
							foot.scrollLeft = pos.x;
					}
				}
			},
			onScrollEnd: function () {
				wgt._doScroll();
			}
		});
	},
	/**
	 * @returns the vertical scroll position of the body element of given MeshWidget.
	 * @param wgt - the widget
	 */
	getScrollPosV(wgt: zul.mesh.MeshWidget): number {
		var bar = wgt._scrollbar;
		if (bar)
			return bar.getCurrentPosition()!.y;

		return wgt.ebody!.scrollTop;
	},
	/**
	 * @returns the horizontal scroll position of the body element of given MeshWidget.
	 * @param wgt - the widget
	 * @since 7.0.0
	 */
	getScrollPosH(wgt: zul.mesh.MeshWidget): number {
		var bar = wgt._scrollbar;
		if (bar)
			return bar.getCurrentPosition()!.x;

		return wgt.ebody!.scrollLeft;
	}
};
zul.mesh.Scrollbar = Scrollbar;