ui-lib/lib/ui/grid/column.js
2024-02-20 14:31:24 +08:00

720 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { global } from "../../utility";
import { createElement } from "../../functions";
import { createIcon } from "../icon";
import { createCheckbox } from "../checkbox";
// import { setTooltip } from "../tooltip";
import { Dropdown } from "../dropdown";
import { convertCssStyle } from "../extension";
import { createDateInput, formatDate, setDateValue, getDateValue } from "../date";
// definition
import { DropdownOptions, GridItemBooleanCallback, GridDropdownSourceCallback, DateFormatterCallback, GridSourceItem, GridItemWrapper, Grid } from "./grid";
/**
* @ignore
* @callback DropExpandedCallback
* @param {Map<string, ValueItem>} item - 行数据对象
* @param {Dropdown} drop - 下拉框对象
* @this GridColumnDefinition
*/
/**
* 列定义接口
* @ignore
* @typedef GridColumnDefinition
* @property {string} key - 列关键字,默认以该关键字从行数据中提取单元格值,行数据的关键字属性值里包含 DisplayValue 则优先显示此值
* @property {(boolean | string | GridItemBooleanCallback)} [enabled] - 列是否可用(可编辑),允许以下类型
*
* * `boolean` 则直接使用该值
* * `string` 则以该值为关键字从行数据中取值作为判断条件
* * `GridItemBooleanCallback` 则调用如下回调,以返回值作为判断条件
* @property {DropdownOptions} [dropOptions] - 列为下拉列表类型时以该值设置下拉框的参数
* @property {(GridSourceItem[] | Promise<GridSourceItem[]> | GridDropdownSourceCallback)} [source] - 列为下拉列表类型时以该值设置下拉列表数据源,支持返回异步对象,也支持如下函数返回
* @property {boolean} [sourceCache=false] - 下拉列表数据源是否缓存结果即行数据未发生变化时仅从source属性获取一次值
* @property {("fa-light" | "fa-regular" | "fa-solid")} [iconType=fa-light] - 列为图标类型时以该值设置图标样式
* @property {string} [dateMin] - 列为日期类型时以该值作为最小可选日期值
* @property {string} [dateMax] - 列为日期类型时以该值作为最大可选日期值
* @property {DateFormatterCallback} [dateValueFormatter] - 列为日期类型时自定义日期格式化函数
* @property {DropExpandedCallback} [onDropExpanded] - 列为下拉框类型时在下拉列表展开时触发的事件
*/
/**
* 列定义基类
*
* _函数调用流程图示_<br/>
* <img src="./assets/column-refresh.jpg" alt="Column Refresh"/>
* @class
* @static
* @hideconstructor
*/
export class GridColumn {
/**
* 设置该类型是否支持触发 {@linkcode GridColumnDefinition}`.onInputEnded` 方法<br/>
* 该属性返回 `true` 后,在任意事件中修改行包装对象的 `__editing` 值,则会在行列元素变动时及时触发 [onInputEnded]{@linkcode GridColumnDefinition#onInputEnded} 方法,避免例如文本框还未触发 `onchange` 事件就被移除元素而导致的问题
*
* 更多例子参考代码中 {@linkcode GridInputColumn} 的实现
* @member
* @name GridColumn.editing
* @readonly
* @type {boolean}
*/
/**
* 创建显示单元格时调用的方法
* @param {GridColumnDefinition} col - 列定义对象
* @param {number} index - 行元素索引(需要配合 [startIndex]{@linkcode Grid#startIndex} 相加得到真实数据索引)
* @param {Grid} grid - Grid 实例
* @returns {HTMLElement} 返回创建的单元格元素
* @virtual
*/
static create() {
return createElement('span');
}
/**
* 创建编辑单元格时调用的方法
*
* 元素修改后设置行包装对象的 `__editing` 后,支持在离开编辑状态时及时触发 [leaveEdit]{@linkcode GridColumn.leaveEdit} 方法<br/>
* 更多例子参考代码中 {@linkcode GridDropdownColumn} 的实现。
* @method
* @name GridColumn.createEdit
* @param {Function} trigger - 编辑事件回调函数,`e` 参数会传递给 [getValue]{@linkcode GridColumn.getValue} 方法
* @param {GridColumnDefinition} col - 列定义对象
* @param {HTMLElement} container - 父容器元素
* @param {GridItemWrapper} wrapper - 行包装对象,其 `values` 属性为行数据对象
* @returns {HTMLElement} 返回创建的编辑状态的单元格元素
* @virtual
*/
/**
* 创建列头时调用的方法
* @method
* @name GridColumn.createCaption
* @param {GridColumnDefinition} col - 列定义对象
* @returns {HTMLElement} 返回创建的列头元素
* @virtual
*/
/**
* 获取编辑状态单元格值时调用的方法
* @method
* @name GridColumn.getValue
* @param {any} e - 由 [createEdit]{@linkcode GridColumn.createEdit} 方法中 `trigger` 函数传递来的对象
* @param {GridColumnDefinition} col - 列定义对象
* @returns {(string | boolean | number)} 返回单元格的值
* @virtual
*/
/**
* 设置单元格值时调用的方法
* @param {HTMLElement} element - 单元格元素
* @param {(string | boolean | number)} val - 待设置的单元格值
* @param {GridItemWrapper} wrapper - 行包装对象
* @param {GridColumnDefinition} col - 列定义对象
* @param {Grid} grid - Grid 对象
* @virtual
*/
static setValue(element, val) {
element.innerText = val;
}
/**
* 设置单元格样式时调用的方法
* @param {HTMLElement} element - 单元格元素
* @param {object} style - 样式对象
* @virtual
*/
static setStyle(element, style) {
// for (let css of Object.entries(style)) {
// element.style.setProperty(css[0], css[1]);
// }
element.style.cssText = convertCssStyle(style);
}
/**
* 设置单元格类名时调用的方法
* @param {HTMLElement} element - 单元格元素
* @param {string} name - 要设置的类名
* @virtual
*/
static setClass(element, name) {
element.className = name ?? '';
}
/**
* 设置单元格可用性时调用的方法
* @param {HTMLElement} element - 单元格元素
* @param {boolean} enabled - 启用值,为 `false` 时代表禁用
* @virtual
*/
static setEnabled(element, enabled) {
const tooltip = element.querySelector('.ui-tooltip-wrapper');
if (tooltip != null) {
tooltip.style.display = enabled === false ? 'none' : '';
}
}
/**
* 单元格离开编辑元素时触发,需要由行包装对象的 `__editing` 来确定是否触发。
* @method
* @name GridColumn.leaveEdit
* @param {HTMLElement} element - 单元格元素
* @param {HTMLElement} container - 父容器元素
* @virtual
*/
/**
* @ignore
*/
static toString() { return '[object Column]' }
}
/**
* 单行文本输入列
* @class
* @static
* @extends GridColumn
* @hideconstructor
*/
export class GridInputColumn extends GridColumn {
static get editing() { return true };
/**
* @ignore
* @param {Function} trigger
* @param {GridColumnDefinition} col
* @param {HTMLElement} _container
* @param {GridItemWrapper} wrapper
* @returns {HTMLElement}
*/
static createEdit(trigger, col, _container, wrapper) {
const input = createElement('input');
input.setAttribute('type', 'text');
if (typeof trigger === 'function') {
input.addEventListener('change', trigger);
}
input.addEventListener('input', () => {
if (wrapper.__editing == null) {
wrapper.__editing = {
[col.key]: true
}
} else {
wrapper.__editing[col.key] = true;
}
});
return input;
}
/**
* @ignore
* @param {HTMLElement} element
* @param {string} val
*/
static setValue(element, val) {
if (element.tagName !== 'INPUT') {
super.setValue(element, val);
} else {
element.value = val;
}
}
/**
* @ignore
* @param {Event} e
* @returns {string}
*/
static getValue(e) { return e.target.value }
/**
* @ignore
* @param {HTMLElement} element
* @param {boolean} enabled
*/
static setEnabled(element, enabled) {
super.setEnabled(element, enabled);
element.disabled = enabled === false;
}
}
/**
* 多行文本输入列
* @class
* @static
* @extends GridInputColumn
* @hideconstructor
* @ignore
*/
export class GridTextColumn extends GridInputColumn {
/**
* @ignore
* @param {Function} trigger
* @param {GridColumnDefinition} col
* @param {HTMLElement} _container
* @param {GridItemWrapper} wrapper
* @returns {HTMLElement}
*/
static createEdit(trigger, col, _container, wrapper) {
const input = createElement('textarea');
if (typeof trigger === 'function') {
input.addEventListener('change', trigger);
}
input.addEventListener('input', () => {
if (wrapper.__editing == null) {
wrapper.__editing = {
[col.key]: true
}
} else {
wrapper.__editing[col.key] = true;
}
});
return input;
}
/**
* @ignore
* @param {HTMLElement} element
* @param {string} val
* @param {GridItemWrapper} _wrapper
* @param {GridColumnDefinition} _col
* @param {Grid} grid
*/
static setValue(element, val, _wrapper, _col, grid) {
if (element.tagName !== 'TEXTAREA') {
super.setValue(element, val);
} else {
element.value = val;
if (val != null) {
const lines = String(val).split('\n').length;
element.style.height = `${lines * grid.lineHeight + 12}px`;
}
// TODO: bad performance
}
}
}
const SymbolDropdown = Symbol.for('ui-dropdown');
/**
* 下拉选择列
* @class
* @static
* @extends GridColumn
* @hideconstructor
*/
export class GridDropdownColumn extends GridColumn {
/**
* @ignore
* @param {Function} trigger
* @param {GridColumnDefinition} col
* @param {HTMLElement} container
* @param {GridItemWrapper} wrapper
* @returns {HTMLElement}
*/
static createEdit(trigger, col, container, wrapper) {
const drop = new Dropdown({
...col.dropOptions,
wrapper: container.parentElement
});
drop.onSelected = trigger;
drop.onExpanded = () => {
if (wrapper.__editing == null) {
wrapper.__editing = {
[col.key]: true
}
} else {
wrapper.__editing[col.key] = true;
}
if (typeof col.onDropExpanded === 'function') {
col.onDropExpanded.call(col, wrapper.values, drop);
}
};
return drop.create();
}
/**
* @private
* @param {HTMLElement} element
* @returns {Dropdown}
*/
static _getDrop(element) {
/**
* @type {Map<string, Dropdown>}
*/
const dropGlobal = global[SymbolDropdown];
if (dropGlobal == null) {
return null;
}
const dropId = element.dataset.dropId;
const drop = dropGlobal[dropId];
if (drop == null) {
return null;
}
return drop;
}
/**
* @private
* @param {GridItemWrapper} wrapper
* @param {GridColumnDefinition} col
* @returns {GridSourceItem[]}
*/
static _getSource(wrapper, col) {
let source;
if (col.sourceCache !== false) {
source = wrapper.source?.[col.key];
if (source != null) {
return source;
}
}
source = col.source;
if (typeof source === 'function') {
source = source(wrapper.values);
}
if (col.sourceCache !== false) {
if (wrapper.source == null) {
wrapper.source = { [col.key]: source };
} else {
wrapper.source[col.key] = source;
}
}
return source;
}
/**
* @private
* @param {GridSourceItem[]} source
* @param {HTMLElement} element
* @param {any} val
* @param {DropdownOptions} opts
*/
static _setValue(source, element, val, opts) {
const data = source?.find(v => v[opts?.valueKey ?? 'value'] === val);
if (data != null) {
val = data[opts?.textKey ?? 'text'];
}
super.setValue(element, val);
}
/**
* @ignore
* @param {HTMLElement} element
* @param {any} val
* @param {GridItemWrapper} wrapper
* @param {GridColumnDefinition} col
*/
static setValue(element, val, wrapper, col) {
if (element.tagName !== 'DIV') {
let source = this._getSource(wrapper, col);
if (source instanceof Promise) {
source.then(s => this._setValue(s, element, val, col.dropOptions));
} else {
this._setValue(source, element, val, col.dropOptions);
}
return;
}
const drop = this._getDrop(element);
if (drop == null) {
return;
}
if (drop.source == null || drop.source.length === 0) {
let source = this._getSource(wrapper, col);
if (source instanceof Promise) {
source.then(s => {
drop.source = s;
drop.select(val, true);
})
return;
} else if (source != null) {
drop.source = source;
}
}
drop.select(val, true);
}
/**
* @ignore
* @param {GridSourceItem} e
* @param {GridColumnDefinition} col
* @returns {any}
*/
static getValue(e, col) {
return e[col.dropOptions?.valueKey ?? 'value'];
}
/**
* @ignore
* @param {HTMLElement} element
* @param {string} name
*/
static setClass(element, name) {
if (element.tagName === 'DIV') {
element.className = `ui-drop-wrapper ${name ?? ''}`;
} else {
super.setClass(element, name);
}
}
/**
* @ignore
* @param {HTMLElement} element
* @param {boolean} enabled
*/
static setEnabled(element, enabled) {
super.setEnabled(element, enabled);
const drop = this._getDrop(element);
if (drop == null) {
return;
}
drop.disabled = enabled === false;
}
/**
* @ignore
* @param {HTMLElement} element
* @param {HTMLElement} container
*/
static leaveEdit(element, container) {
container.parentElement.querySelectorAll('.ui-drop-box.active').forEach(e => {
if (e != null) {
e.classList.remove('active');
}
});
const drop = this._getDrop(element);
if (drop == null) {
return;
}
if (drop?.multiSelect && typeof drop.onCollapsed === 'function') {
drop.onCollapsed();
}
}
}
/**
* 复选框列
* @class
* @static
* @extends GridColumn
* @hideconstructor
* @ignore
*/
export class GridCheckboxColumn extends GridColumn {
/**
* @ignore
* @param {Function} trigger
* @returns {HTMLElement}
*/
static createEdit(trigger) {
const check = createCheckbox({
onchange: typeof trigger === 'function' ? trigger : null
});
return check;
}
/**
* @ignore
* @param {HTMLElement} element
* @param {boolean} val
*/
static setValue(element, val) {
element.querySelector('input').checked = val;
}
/**
* @ignore
* @param {Event} e
* @returns {boolean}
*/
static getValue(e) { return e.target.checked }
/**
* @ignore
* @param {HTMLElement} element
* @param {string} name
*/
static setClass(element, name) {
if (element.tagName === 'LABEL') {
element.className = `ui-check-wrapper ${name ?? ''}`;
} else {
super.setClass(element, name);
}
}
/**
* @ignore
* @param {HTMLElement} element
* @param {boolean} enabled
*/
static setEnabled(element, enabled) {
super.setEnabled(element, enabled);
element.querySelector('input').disabled = enabled === false;
}
}
/**
* 图标列
* @class
* @static
* @extends GridColumn
* @hideconstructor
* @ignore
*/
export class GridIconColumn extends GridColumn {
/**
* @ignore
* @returns {HTMLElement}
*/
static create() { return createElement('span', 'col-icon') }
/**
* @ignore
* @param {HTMLElement} element
* @param {string} val
* @param {GridItemWrapper} wrapper
* @param {GridColumnDefinition} col
*/
static setValue(element, val, wrapper, col) {
// let className = col.iconClassName;
// if (typeof className === 'function') {
// className = className.call(col, wrapper.values);
// }
// if (className == null) {
// element.className = 'col-icon';
// } else {
// element.className = `col-icon ${className}`;
// }
let type = col.iconType;
if (typeof type === 'function') {
type = type.call(col, wrapper.values);
}
type ??= 'fa-light';
if (element.dataset.type !== type || element.dataset.icon !== val) {
const icon = createIcon(type, val);
// const layer = element.children[0];
element.replaceChildren(icon);
// !nullOrEmpty(col.tooltip) && setTooltip(element, col.tooltip, false, grid.element);
element.dataset.type = type;
element.dataset.icon = val;
}
}
/**
* @ignore
* @param {HTMLElement} element
* @param {string} name
*/
static setClass(element, name) {
element.className = `col-icon ${name ?? ''}`;
}
/**
* @ignore
* @param {HTMLElement} element
* @param {boolean} enabled
*/
static setEnabled(element, enabled) {
super.setEnabled(element, enabled);
if (enabled === false) {
element.classList.add('disabled');
} else {
element.classList.remove('disabled');
}
}
}
/**
* 日期选择列
* @class
* @static
* @extends GridColumn
* @hideconstructor
*/
export class GridDateColumn extends GridColumn {
static get editing() { return true };
/**
* @ignore
* @param {Function} trigger
* @param {GridColumnDefinition} col
* @param {HTMLElement} _container
* @param {GridItemWrapper} wrapper
* @returns {HTMLElement}
*/
static createEdit(trigger, col, _container, wrapper) {
let enabled = col.enabled;
if (typeof enabled === 'string') {
enabled = wrapper.values[enabled];
} else if (typeof enabled === 'function') {
enabled = col.enabled(wrapper.values);
}
if (enabled === false) {
return super.create();
}
const date = createDateInput(col.dateMin, col.dateMax);
date.addEventListener('change', () => {
if (wrapper.__editing == null) {
wrapper.__editing = {
[col.key]: true
}
} else {
wrapper.__editing[col.key] = true;
}
});
date.addEventListener('blur', trigger);
return date;
}
/**
* @ignore
* @param {HTMLElement} element
* @param {(string | number | Date)} val
*/
static setValue(element, val) {
setDateValue(element, val);
}
/**
* @ignore
* @param {Event} e
* @param {GridColumnDefinition} col
* @returns {string}
*/
static getValue(e, col) {
return getDateValue(e.target, col.dateValueFormatter);
}
/**
* @ignore
* @param {HTMLElement} element
* @param {string} name
*/
static setClass(element, name) {
if (element.tagName === 'INPUT') {
element.className = `ui-date-cell ${name ?? ''}`;
} else {
super.setClass(element, name);
}
}
/**
* @ignore
* @param {HTMLElement} element
* @param {boolean} enabled
*/
static setEnabled(element, enabled) {
element.disabled = enabled === false;
}
/**
* 格式化日期字符串
* @param {(string | number | Date)} date - 要格式化的日期值
*
* 支持以下几种数据类型
* * `"2024-01-26"`
* * `"1/26/2024"`
* * `"638418240000000000"`
* * `new Date('2024-01-26')`
* @returns {string} 格式化为 M/d/yyyy 的日期字符串
*/
static formatDate(date) {
return formatDate(date);
}
}