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 */ /** * 列定义基类 * * _函数调用流程图示_
* 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} 方法
* 更多例子参考代码中 {@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} */ 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' } }