ui-lib/lib/ui/grid/column.js

869 lines
24 KiB
JavaScript

import { global } from "../../utility";
import { createElement } from "../../functions";
import { createIcon } from "../icon";
import { createCheckbox, createRadiobox } from "../checkbox";
// import { setTooltip } from "../tooltip";
import { Dropdown } from "../dropdown";
import { convertCssStyle } from "../extension";
import { createDateInput, formatDate, setDateValue, getDateValue } from "../date";
// definition
import { GridRowItem, DropdownOptions, GridColumnDefinition, GridSourceItem, GridItemWrapper, Grid } from "./grid";
/**
* @ignore
* @callback DropExpandedCallback
* @param {GridRowItem} item - 行数据对象
* @param {Dropdown} drop - 下拉框对象
* @this GridColumnDefinition
*/
/**
* 列定义基类
*
* _函数调用流程图示_<br/>
* <img src="./assets/column-refresh.jpg" alt="Column Refresh"/>
* @class
* @static
* @hideconstructor
*/
export class GridColumn {
/**
* 该属性返回 `true` 后,在任意事件中修改行包装对象的 `__editing` 值,则会在行列元素变动时及时触发 [onChanged]{@linkcode GridColumnDefinition#onChanged} 方法,避免例如文本框和日期框还未触发事件就被移除元素而导致的问题
* @member
* @name GridColumn.editing
* @readonly
* @type {boolean}
* @see 更多例子参考 {@linkcode GridInputColumn} {@linkcode GridDateColumn} 中的代码实现
*/
/**
* 标记该类型是否可编辑
* @member
* @name GridColumn.canEdit
* @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 - 编辑事件回调函数
* @param {any} 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.getElement
* @param {HTMLElement} element - 单元格主内容元素
* @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` 时代表禁用
* @param {boolean} editing - 是否处于编辑状态
* @virtual
*/
static setEnabled(element, enabled) {
const tooltip = element.querySelector('.ui-tooltip-wrapper');
if (tooltip != null) {
tooltip.style.display = enabled === false ? 'none' : '';
}
}
/**
* 单元格编辑状态发生改变时调用的方法
* @method
* @name GridColumn.setEditing
* @param {HTMLElement} element - 单元格元素
* @param {boolean} editing - 是否处于编辑状态
* @virtual
*/
/**
* 单元格离开编辑元素时调用的方法,需要由行包装对象的 `__editing` 来确定是否触发。
* @method
* @name GridColumn.leaveEdit
* @param {HTMLElement} element - 单元格元素
* @param {HTMLElement} container - 父容器元素
* @virtual
*/
/**
* @ignore
*/
static toString() { return 'GridCommon' }
/**
* @ignore
* @param {string} key
* @param {GridItemWrapper} wrapper
* @param {any} value
*/
static _changeValue(key, wrapper, value) {
const val = wrapper.values[key] ?? null;
const hasValue = val != null && Object.prototype.hasOwnProperty.call(val, 'Value');
if (wrapper.__editing == null) {
wrapper.__editing = {
[key]: hasValue ? val.Value : val
}
} else if (!Object.prototype.hasOwnProperty.call(wrapper.__editing, key)) {
wrapper.__editing[key] = hasValue ? val.Value : val;
}
if (hasValue) {
val.Value = value;
if (Object.prototype.hasOwnProperty.call(val, 'DisplayValue')) {
val.DisplayValue = value;
}
} else {
wrapper.values[key] = value;
}
}
}
/**
* 单行文本输入列
* @class
* @static
* @extends GridColumn
* @hideconstructor
*/
export class GridInputColumn extends GridColumn {
static get editing() { return true };
static get canEdit() { 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');
input.addEventListener('input', () => super._changeValue(col.key, wrapper, input.value));
input.addEventListener('change', trigger);
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;
}
/**
* @ignore
*/
static toString() { return 'GridInput' }
}
/**
* 多行文本输入列
* @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');
input.addEventListener('input', () => super._changeValue(col.key, wrapper, input.value));
input.addEventListener('change', trigger);
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
}
}
/**
* @ignore
*/
static toString() { return 'GridText' }
}
const SymbolDropdown = Symbol.for('ui-dropdown');
/**
* 下拉选择列
* @class
* @static
* @extends GridColumn
* @hideconstructor
*/
export class GridDropdownColumn extends GridColumn {
static get canEdit() { return true };
/**
* @ignore
* @returns {HTMLElement}
*/
static create() {
return createElement('span', 'ui-drop-span', createElement('span'));
}
/**
* @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();
}
/**
* @ignore
* @param {HTMLElement} element
* @returns {HTMLElement}
*/
static getElement(element) {
if (element.tagName === 'DIV') {
return element.children[0].children[0];
}
return element.children[0];
}
/**
* @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'];
}
element.children[0].innerText = val;
}
/**
* @ignore
* @param {HTMLElement} element
* @param {any} val
* @param {GridItemWrapper} wrapper
* @param {GridColumnDefinition} col
* @param {Grid} grid
*/
static setValue(element, val, wrapper, col, grid) {
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;
}
const ignoreCase = col.dropRestrictCase !== true;
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, ignoreCase);
})
return;
} else if (source != null) {
drop.source = source;
}
}
if (typeof val === 'string' && val !== '') {
const lVal = String(val).toLowerCase();
const item = drop.source.find(s => {
let v = s[col.dropOptions?.valueKey ?? 'value'];
if (ignoreCase) {
return String(v).toLowerCase() === lVal;
}
return v === val;
});
if (item == null) {
let text;
if (col.text == null && typeof col.filter === 'function') {
text = col.filter(wrapper.values, false, grid._var.refs.body);
} else {
text = val;
}
drop.source.push({
[col.dropOptions?.textKey ?? 'text']: text,
[col.dropOptions?.valueKey ?? 'value']: val
});
}
}
drop.select(val, true, ignoreCase);
}
/**
* @ignore
* @param {GridSourceItem} e
* @param {GridColumnDefinition} col
* @returns {any}
*/
static getValue(e, col) {
return {
value: e[col.dropOptions?.valueKey ?? 'value'],
text: e[col.dropOptions?.textKey ?? 'text']
};
}
/**
* @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();
}
}
/**
* @ignore
*/
static toString() { return 'GridDropdown' }
}
/**
* 复选框列
* @class
* @static
* @extends GridColumn
* @hideconstructor
* @ignore
*/
export class GridCheckboxColumn extends GridColumn {
static get canEdit() { return true };
/**
* @ignore
* @param {Function} trigger
* @returns {HTMLElement}
*/
static createEdit(trigger) {
const check = createCheckbox({
onchange: trigger
});
return check;
}
/**
* @ignore
* @param {HTMLElement} element
* @param {boolean} val
*/
static setValue(element, val) {
// element.querySelector('input').checked = val;
element.children[0].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;
element.children[0].disabled = enabled === false;
}
/**
* @ignore
*/
static toString() { return 'GridCheckbox' }
}
/**
* 单选框列
* @class
* @static
* @extends GridCheckboxColumn
* @hideconstructor
* @ignore
*/
export class GridRadioboxColumn extends GridCheckboxColumn {
/**
* @ignore
* @param {Function} trigger
* @param {GridColumnDefinition} _col
* @param {number} index
* @returns {HTMLElement}
*/
static createEdit(trigger, _col, index) {
const check = createRadiobox({
name: `r_${index}`,
onchange: trigger
});
return check;
}
/**
* @ignore
*/
static toString() { return 'GridRadiobox' }
}
/**
* 图标列
* @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');
}
}
/**
* @ignore
*/
static toString() { return 'GridIcon' }
}
/**
* 日期选择列
* @class
* @static
* @extends GridColumn
* @hideconstructor
*/
export class GridDateColumn extends GridColumn {
static get editing() { return true };
static get canEdit() { 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', () => super._changeValue(col.key, wrapper, date.value));
date.addEventListener('blur', trigger);
return date;
}
/**
* @ignore
* @param {HTMLElement} element
* @param {(string | number | Date)} val
* @param {GridItemWrapper} _wrapper
* @param {GridColumnDefinition} col
*/
static setValue(element, val, _wrapper, col) {
setDateValue(element, val, col.dateDisplayFormatter);
}
/**
* @ignore
* @param {Event} e
* @param {GridColumnDefinition} col
* @returns {string}
*/
static getValue(e, col) {
if (e.target.tagName === 'INPUT') {
return {
value: getDateValue(e.target, col.dateValueFormatter),
text: getDateValue(e.target, col.dateDisplayFormatter)
};
}
return e.target.innerText;
}
/**
* @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')`
* @param {string} [formatter] - 格式化格式,默认为 `"m/d/Y"`
*
* * Y - 年,例如 `2024`
* * y - 年的后两位,例如 `"24"`
* * n - 月,例如 `2`
* * m - 格式化成两位的月,例如 `"02"`
* * j - 日,例如 `4`
* * d - 格式化成两位的日,例如 `"04"`
* * t - 日期当月总天数,例如 `29`
* * L - 是否为闰年,例如 `1`
* * G - 小时,例如 `15`
* * g - 12 小时制的小时,例如 `3`
* * H - 格式化成两位的小时,例如 `"15"`
* * h - 格式化成两位的 12 小时制的小时,例如 `"03"`
* * A - 上下午,例如 `"PM"`
* * a - 小写的上下午,例如 `"pm"`
* * i - 格式化成两位的分钟,例如 `"39"`
* * s - 格式化成两位的秒,例如 `"20"`
* * u - 格式化成六位的毫秒,例如 `"023040"`
* * e - 时区描述字符串,例如 `"China Standard Time"`
* * O - 时区偏移字符串,例如 `"+0800"`
* * P - `"+08:00"` 这种格式的时区偏移字符串
* @returns {string} 格式化为 M/d/yyyy 的日期字符串
*/
static formatDate(date, formatter) {
return formatDate(date, formatter);
}
/**
* @ignore
*/
static toString() { return 'GridDate' }
}