feature: tooltip in the checkbox

feature: add GridColumn.getElement
feature: column property of `contentWrap` and `maxLines`
change: line height from 24px to 18px
fix: adapt enabled after cell changed
fix: wrap issue of filter panel
This commit is contained in:
2024-03-07 12:54:00 +08:00
parent 6fb7c3c769
commit 168cae3ce1
8 changed files with 343 additions and 126 deletions

View File

@ -14,6 +14,8 @@ interface CheckboxOptions {
type?: string;
/** 标签 */
label?: string | HTMLElement;
/** 标签提示文本 */
title?: string;
/** 是否已选中 */
checked?: boolean;
/** 图片高度 */

View File

@ -2,7 +2,7 @@ import './css/checkbox.scss';
import { createElement } from "../functions";
import { createIcon } from "./icon";
function fillCheckbox(container, type = 'fa-regular', label, tabindex = -1, charactor = 'check') {
function fillCheckbox(container, type = 'fa-regular', label, tabindex = -1, charactor = 'check', title) {
container.appendChild(
createElement('layer', layer => {
layer.className = 'ui-check-inner';
@ -24,7 +24,10 @@ function fillCheckbox(container, type = 'fa-regular', label, tabindex = -1, char
container.appendChild(label);
} else if (label != null && String(label).length > 0) {
container.appendChild(
createElement('span', span => span.innerText = label)
createElement('span', span => {
span.innerText = label;
span.title = title;
})
);
}
}
@ -52,7 +55,7 @@ export function createRadiobox(opts = {}) {
if (opts.className) {
container.classList.add(opts.className);
}
fillCheckbox(container, opts.type, opts.label, opts.tabIndex, 'circle');
fillCheckbox(container, opts.type, opts.label, opts.tabIndex, 'circle', opts.title);
return container;
}
@ -92,7 +95,7 @@ export function createCheckbox(opts = {}) {
opts.uncheckedNode.classList.add('unchecked');
container.appendChild(opts.uncheckedNode);
} else {
fillCheckbox(container, opts.type, opts.label, opts.tabIndex);
fillCheckbox(container, opts.type, opts.label, opts.tabIndex, undefined, opts.title);
}
return container;
}
@ -144,7 +147,7 @@ export function resolveCheckbox(container = document.body, legacy) {
label.className = 'ui-check-wrapper';
}
label.replaceChildren();
fillCheckbox(label, 'fa-regular', text, chk.tabIndex);
fillCheckbox(label, 'fa-regular', text, chk.tabIndex, undefined, label.title);
label.insertBefore(chk, label.firstChild);
}
}
@ -161,7 +164,10 @@ export function resolveCheckbox(container = document.body, legacy) {
fillCheckbox(box,
box.dataset.type,
box.dataset.label,
box.dataset.tabIndex)
box.dataset.tabIndex,
undefined,
box.title);
box.removeAttribute('title');
box.removeAttribute('data-type');
box.removeAttribute('data-label');
}

View File

@ -44,7 +44,7 @@
--header-padding: 4px 12px 4px 8px;
--header-filter-padding: 4px 26px 4px 8px;
--spacing-s: 4px;
--spacing-cell: 6px 4px 6px 8px;
--spacing-cell: 9px 4px 9px 8px;
--filter-line-height: 30px;
--filter-item-padding: 0 4px;
}
@ -291,6 +291,10 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: pre;
&.wrap {
white-space: normal;
}
}
}
}
@ -357,6 +361,7 @@
}
>input[type="text"],
>input[type="date"],
>textarea {
border: none;
box-sizing: border-box;
@ -415,12 +420,8 @@
}
.ui-date-cell {
line-height: 22px;
box-sizing: border-box;
padding: var(--spacing-cell);
border: none;
font-size: var(--font-size);
width: 100%;
height: var(--row-height);
text-indent: 4px;
&:invalid {
color: rgba(0, 0, 0, .3);
@ -581,6 +582,7 @@
.ui-check-wrapper {
height: var(--filter-line-height);
line-height: var(--filter-line-height);
display: flex;
.ui-check-inner+* {

View File

@ -35,7 +35,7 @@
--border-radius: 2px;
--text-indent: 4px;
--line-height: 24px;
--line-height: 18px;
--font-size: .8125rem; // 13px
--font-smaller-size: .75rem; // 12px

View File

@ -73,6 +73,15 @@ export class GridColumn {
* @virtual
*/
/**
* 获取用于判断文本大小的元素
* @method
* @name GridColumn.getElement
* @param {HTMLElement} element - 单元格主内容元素
* @returns {HTMLElement} 返回文本元素
* @virtual
*/
/**
* 获取编辑状态单元格值时调用的方法
* @method
@ -153,7 +162,7 @@ export class GridColumn {
/**
* @ignore
*/
static toString() { return '[object Column]' }
static toString() { return 'GridCommon' }
/**
* @ignore
@ -234,6 +243,11 @@ export class GridInputColumn extends GridColumn {
super.setEnabled(element, enabled);
element.disabled = enabled === false;
}
/**
* @ignore
*/
static toString() { return 'GridInput' }
}
/**
@ -280,6 +294,11 @@ export class GridTextColumn extends GridInputColumn {
// TODO: bad performance
}
}
/**
* @ignore
*/
static toString() { return 'GridText' }
}
const SymbolDropdown = Symbol.for('ui-dropdown');
@ -477,6 +496,11 @@ export class GridDropdownColumn extends GridColumn {
drop.onCollapsed();
}
}
/**
* @ignore
*/
static toString() { return 'GridDropdown' }
}
/**
@ -538,6 +562,11 @@ export class GridCheckboxColumn extends GridColumn {
super.setEnabled(element, enabled);
element.querySelector('input').disabled = enabled === false;
}
/**
* @ignore
*/
static toString() { return 'GridCheckbox' }
}
/**
@ -609,6 +638,11 @@ export class GridIconColumn extends GridColumn {
element.classList.remove('disabled');
}
}
/**
* @ignore
*/
static toString() { return 'GridIcon' }
}
/**
@ -700,4 +734,9 @@ export class GridDateColumn extends GridColumn {
static formatDate(date) {
return formatDate(date);
}
/**
* @ignore
*/
static toString() { return 'GridDate' }
}

View File

@ -13,7 +13,7 @@ import { GridColumn, GridInputColumn, GridTextColumn, GridDropdownColumn, GridCh
/**
* @author Tsanie Lily <tsorgy@gmail.com>
* @license MIT
* @version 1.0.1
* @version 1.0.2
*/
const ColumnChangedType = {
@ -234,7 +234,9 @@ let r = lang;
* @property {boolean} [orderable=true] - 列是否允许重排顺序
* @property {boolean} [allcheck=false] - 列为复选框类型时是否在列头增加全选复选框
* @property {boolean} [shrink=false] - 列为收缩列,禁用自动调整大小
* @property {string} [class] - 单元格元素的额外样式类型字符串(仅在重建行元素时读取
* @property {string} [class] - 单元格元素的额外样式类型字符串(仅在重建行元素时设置
* @property {boolean} [contentWrap=false] - 单元格文本是否换行(仅在重建行元素时设置)
* @property {number} [maxLines=0] - 大于 0 时限制显示最大行数
* @property {any} [css] - 单元格css样式对象仅在重建行元素时读取
* @property {any} [totalCss] - 合计行样式(仅在重建合计行元素时读取)
* @property {(any | GridItemObjectCallback)} [style] - 单元格样式(填充行列数据时读取),支持直接返回样式对象或调用函数返回(若赋值则忽略 [styleFilter]{@linkcode GridColumnDefinition#styleFilter}
@ -588,6 +590,12 @@ export class Grid {
* @private
*/
currentSource: null,
/**
* 列可用性的关联字典
* @type {KeyMap<string | boolean>}
* @private
*/
enabledDict: {},
/**
* 合计行数据
* @type {GridRowItem}
@ -825,10 +833,10 @@ export class Grid {
/**
* 文本行高(多行文本列计算高度时使用)
* @type {number}
* @default 24
* @default 18
* @ignore
*/
lineHeight = 24;
lineHeight = 18;
/**
* 列头未过滤时的图标
* @type {string}
@ -1046,7 +1054,7 @@ export class Grid {
* @property {number} [virtualCount=100] - 行数大于等于该值则启用虚模式
* @property {boolean} [autoResize=true] - 未设置宽度的列自动调整列宽
* @property {number} [rowHeight=36] - 表格行高
* @property {number} [lineHeight=24] - 文本行高(多行文本列计算高度时使用)
* @property {number} [lineHeight=18] - 文本行高(多行文本列计算高度时使用)
* @property {string} [filterIcon=ellipsis-h] - 列头未过滤时的图标
* @property {string} [filteredIcon=filter] - 列头已过滤时的图标
* @property {number} [extraRows=0] - 列表底部留出额外的空白行
@ -2062,6 +2070,78 @@ export class Grid {
return array;
}
/**
* 导出已压缩的数据源,结构为
* ```
* {
* columns: [],
* source: [],
* rowHeight: number,
* sortDirection: number,
* sortKey?: string,
* sortArray?: Array<{
* column: string,
* order: "asc" | "desc"
* }>
* }
* ```
* @returns {Promise<Uint8Array>} 返回 `Uint8Array` 数据对象
* @since 1.0.2
*/
export() {
const js = new Blob(['function h(e,t,o){if(null==e)return"";let r;const l={},h={};let s="",n="",p="",a=2,f=3,c=2;const u=[];let i=0,d=0;for(let w=0;w<e.length;w+=1)if(s=e.charAt(w),Object.prototype.hasOwnProperty.call(l,s)||(l[s]=f++,h[s]=!0),n=p+s,Object.prototype.hasOwnProperty.call(l,n))p=n;else{if(Object.prototype.hasOwnProperty.call(h,p)){if(p.charCodeAt(0)<256){for(let e=0;e<c;e++)i<<=1,d==t-1?(d=0,u.push(o(i)),i=0):d++;r=p.charCodeAt(0);for(let e=0;e<8;e++)i=i<<1|1&r,d==t-1?(d=0,u.push(o(i)),i=0):d++,r>>=1}else{r=1;for(let e=0;e<c;e++)i=i<<1|r,d==t-1?(d=0,u.push(o(i)),i=0):d++,r=0;r=p.charCodeAt(0);for(let e=0;e<16;e++)i=i<<1|1&r,d==t-1?(d=0,u.push(o(i)),i=0):d++,r>>=1}0==--a&&(a=Math.pow(2,c),c++),delete h[p]}else{r=l[p];for(let e=0;e<c;e++)i=i<<1|1&r,d==t-1?(d=0,u.push(o(i)),i=0):d++,r>>=1}0==--a&&(a=Math.pow(2,c),c++),l[n]=f++,p=String(s)}if(""!==p){if(Object.prototype.hasOwnProperty.call(h,p)){if(p.charCodeAt(0)<256){for(let e=0;e<c;e++)i<<=1,d==t-1?(d=0,u.push(o(i)),i=0):d++;r=p.charCodeAt(0);for(let e=0;e<8;e++)i=i<<1|1&r,d==t-1?(d=0,u.push(o(i)),i=0):d++,r>>=1}else{r=1;for(let e=0;e<c;e++)i=i<<1|r,d==t-1?(d=0,u.push(o(i)),i=0):d++,r=0;r=p.charCodeAt(0);for(let e=0;e<16;e++)i=i<<1|1&r,d==t-1?(d=0,u.push(o(i)),i=0):d++,r>>=1}0==--a&&(a=Math.pow(2,c),c++),delete h[p]}else{r=l[p];for(let e=0;e<c;e++)i=i<<1|1&r,d==t-1?(d=0,u.push(o(i)),i=0):d++,r>>=1}0==--a&&(a=Math.pow(2,c),c++)}r=2;for(let e=0;e<c;e++)i=i<<1|1&r,d==t-1?(d=0,u.push(o(i)),i=0):d++,r>>=1;let w=!0;do{i<<=1,d==t-1?(u.push(o(i)),w=!1):d++}while(w);return u.join("")}function s(e){return null==e?"":h(e,16,e=>String.fromCharCode(e))}function i(e){const t=s(e),o=new Uint8Array(2*t.length);for(let e=0,r=t.length;e<r;e++){const r=t.charCodeAt(e);o[2*e]=r>>>8,o[2*e+1]=r%256}return o}self.addEventListener("message",function(e){this.self.postMessage(i(e.data))},!1);']);
return new Promise((resolve, reject) => {
let working;
const url = URL.createObjectURL(js);
const worker = new Worker(url);
/**
* @private
* @param {Function} next
* @param {any} data
*/
const terminate = (next, data) => {
working = false;
worker.terminate();
URL.revokeObjectURL(url);
next(data);
}
// 超过 10 秒则拒绝
const timer = setTimeout(() => {
if (working) {
terminate(reject, { message: 'timeout' });
}
}, 10000);
worker.addEventListener('message', e => {
if (working) {
clearTimeout(timer);
terminate(resolve, e.data);
}
})
worker.addEventListener('error', e => {
if (working) {
clearTimeout(timer);
terminate(reject, e);
}
})
working = true;
worker.postMessage(JSON.stringify({
columns: this.columns.map(c => ({
key: c.key,
type: c.type?.toString(),
caption: c.caption,
width: c.width,
align: c.align,
visible: c.visible
})),
source: this.source,
rowHeight: this.rowHeight,
sortDirection: this.sortDirection,
sortKey: this.sortKey,
sortArray: this.sortArray
}));
});
}
/**
* @private
* @callback PrivateGridComparerCallback
@ -2387,10 +2467,13 @@ export class Grid {
if (style !== '') {
cell.style.cssText = style;
}
const element = GridColumn.create(col)
const element = GridColumn.create(col);
if (typeof col.class === 'string') {
GridColumn.setClass(element, col.class);
}
if (col.contentWrap) {
element.classList.add('wrap');
}
cell.appendChild(element);
} else {
cell.style.display = 'none';
@ -2474,6 +2557,9 @@ export class Grid {
if (typeof col.class === 'string') {
type.setClass(element, col.class);
}
if (col.contentWrap) {
element.classList.add('wrap');
}
}
cell.appendChild(element);
if (col.events != null) {
@ -2649,6 +2735,9 @@ export class Grid {
if (typeof col.class === 'string') {
type.setClass(element, col.class);
}
if (col.contentWrap) {
element.classList.add('wrap');
}
cell.replaceChildren(element);
if (col.events != null) {
for (let ev of Object.entries(col.events)) {
@ -2666,6 +2755,7 @@ export class Grid {
delete virtualCell.attrs;
delete virtualCell.style;
delete virtualCell.value;
delete virtualCell.enabled;
}
if (typeof type.setEditing === 'function') {
type.setEditing(element, virtualRow.editing);
@ -2675,15 +2765,17 @@ export class Grid {
virtualCell.value = val;
type.setValue(element, val, vals, col, this);
}
if (virtualRow.editing && typeof type.setEnabled === 'function') {
if (typeof type.setEnabled === 'function') {
let enabled;
if (readonly) {
enabled = false;
} else {
enabled = col.enabled;
if (typeof enabled === 'function') {
this._var.enabledDict[col.key] = true;
enabled = enabled.call(col, item);
} else if (typeof enabled === 'string') {
this._var.enabledDict[col.key] = enabled;
enabled = item[enabled];
}
}
@ -2720,6 +2812,14 @@ export class Grid {
} else if (typeof col.styleFilter === 'function') {
style = col.styleFilter(item);
}
if (col.maxLines > 0) {
const maxHeight = `${col.maxLines * this.lineHeight}px`;
if (style == null) {
style = { 'max-height': maxHeight };
} else {
style['max-height'] = maxHeight;
}
}
const styleText = style != null ? convertCssStyle(style) : '';
if (styleText !== virtualCell.style) {
virtualCell.style = styleText;
@ -3420,9 +3520,11 @@ export class Grid {
_doFillFilterList(content, array, all) {
for (let item of array) {
const div = createElement('div', 'filter-item');
const display = Object.prototype.hasOwnProperty.call(item, 'DisplayValue') ? item.DisplayValue : item;
div.appendChild(createCheckbox({
checked: item.__checked,
label: Object.prototype.hasOwnProperty.call(item, 'DisplayValue') ? item.DisplayValue : item,
label: display && String(display).replace(/(\r\n|\n|<br[ \t]*\/?>)/g, '\u00a0'),
title: display,
onchange: e => {
item.__checked = e.target.checked;
all.querySelector('input').checked = ![...content.querySelectorAll('input')].some(i => !i.checked);
@ -3708,7 +3810,25 @@ export class Grid {
// sub ui-grid
return;
}
const element = target.children[0];
const row = target.dataset.row;
if (this._var.virtualRows[row]?.editing) {
delete holder.dataset.row;
delete holder.dataset.col;
if (holder.classList.contains('active')) {
holder.classList.remove('active');
}
return;
}
const col = target.dataset.col;
if (holder.dataset.row === row &&
holder.dataset.col === col) {
return;
}
const type = this._var.colTypes[this.columns[col]?.key];
let element = target.children[0];
if (type != null && typeof type.getElement === 'function') {
element = type.getElement(element);
}
if (element?.tagName !== 'SPAN') {
if (holder.classList.contains('active')) {
delete holder.dataset.row;
@ -3717,13 +3837,8 @@ export class Grid {
}
return;
}
const row = target.dataset.row;
const col = target.dataset.col;
if (holder.dataset.row === row &&
holder.dataset.col === col) {
return;
}
if (element.scrollWidth > element.offsetWidth) {
if (element.scrollWidth > element.offsetWidth ||
element.scrollHeight > element.offsetHeight) {
holder.dataset.row = row;
holder.dataset.col = col;
holder.innerText = element.innerText;
@ -3738,7 +3853,7 @@ export class Grid {
left = maxleft;
}
const height = target.offsetHeight;
holder.style.cssText = `top: ${top}px; left: ${left}px; max-width: ${this._var.wrapClientWidth}px; height: ${height - 2}px`;
holder.style.cssText = `top: ${top}px; left: ${left}px; max-width: ${this._var.wrapClientWidth}px; min-height: ${height - 2}px`;
holder.classList.add('active');
} else if (holder.classList.contains('active')) {
delete holder.dataset.row;
@ -3852,9 +3967,9 @@ export class Grid {
if (this._var.currentSource == null) {
return;
}
const row = this._var.currentSource[this._var.startIndex + index];
delete row.source;
const item = row.values;
const vals = this._var.currentSource[this._var.startIndex + index];
delete vals.source;
const item = vals.values;
if (item == null) {
return;
}
@ -3873,7 +3988,8 @@ export class Grid {
oldValue ??= val;
item[col.key] = value;
}
const virtualCell = this._var.virtualRows[index]?.cells[col.key];
const virtualRow = this._var.virtualRows[index];
const virtualCell = virtualRow.cells[col.key];
if (virtualCell != null) {
virtualCell.value = value;
}
@ -3886,7 +4002,36 @@ export class Grid {
} else {
setTooltip(cell.children[0], tip, false, this.element);
}
row.__changed = true;
// 调整其他列的可用性
const row = this._tableRows[index];
const offset = this.expandable ? 1 : 0;
this.columns.forEach((c, j) => {
const cache = this._var.enabledDict[c.key];
if (cache !== true && cache !== col.key) {
return;
}
const cell = row.children[j + offset];
if (cell == null) {
return;
}
const type = GridColumnTypeEnum.isCheckbox(c.type) ?
GridCheckboxColumn : this._var.colTypes[c.key] ?? GridColumn;
if (typeof type.setEnabled === 'function') {
if (typeof c.enabled === 'function') {
enabled = c.enabled(item);
} else if (typeof c.enabled === 'string') {
enabled = item[c.enabled];
} else {
return;
}
const vCell = virtualRow.cells[c.key ?? j];
if (enabled !== vCell.enabled) {
vCell.enabled = enabled;
type.setEnabled(cell.children[0], enabled);
}
}
});
vals.__changed = true;
if (typeof col.onChanged === 'function') {
col.onChanged.call(this, item, value, oldValue, e);
}