@wiajs/ui
Version:
wia ui packages
1,402 lines (1,399 loc) • 178 kB
JavaScript
/** @jsxImportSource @wiajs/core */ /**
* 在线编辑表格
*/ import { jsx as _jsx, jsxs as _jsxs } from "@wiajs/core/jsx-runtime";
import { Event } from '@wiajs/core';
import { isDate, promisify } from '@wiajs/core/util/tool';
import { log as Log } from '@wiajs/util';
import DataTable, { col, th } from '../dataTable';
import { edit as editAttach, fillAttach, view as viewAttach } from './attach';
import * as chip from './chip';
import * as tool from './tool';
const log = Log({
m: 'editTable'
}) // 创建日志实例
;
/**
* @typedef {import('jquery')} $
* @typedef {JQuery} Dom
*/ /** @typedef {object} Opts
* @prop {Dom} [el] - contain
* @prop {Dom} [tb] - $table
* @prop {string} [name]
* @prop {string[]} [editTag] - 可编辑元素标签
* @prop {boolean} [edit] - 编辑模式
* @prop {boolean} [newEdit] - 新编辑
* @prop {boolean} [add] - 新增模式
* @prop {boolean} [kv] - key value
* @prop {number} [col] - 最大列数
* @prop {number[]} [colWidth] - 列宽
* @prop {number} [viewid] - 数据卡id
* @prop {*} [upload] - 上传接口
* @prop {*} [updateJson] - 编辑卡片json
* @prop {*[]} [head] - 非kv模式表头
* @prop {*[]} [data] - 非kv模式数据
* @prop {*[]} [use] - 插件,如果带data,则需带插件,否则附件加载有问题
*/ // * @prop {{url:string, token: string, param:*}} [api]
/** @typedef {object} Opt
* @prop {Dom} tb - $table
* @prop {Dom} el - contain
* @prop {string} name
* @prop {string} domProp
* @prop {string[]} editTag
* @prop {boolean} edit
* @prop {boolean} newEdit
* @prop {boolean} add
* @prop {boolean} kv - key value
* @prop {number} labelWidth - label 宽度 百分比
* @prop {number} col - 最大列数
* @prop {number[]} [colWidth] - 列宽
* @prop {number} [colRatio] - 列比
* @prop {number} [viewid] - 数据卡id
* @prop {*} [upload] - 上传接口
* @prop {*} [prjid] - 项目id
* @prop {*} [getSelAll] - 公司列表接口
* @prop {*} [saveEmbTb] - 保存表格接口
* @prop {*} [updateJson] - 编辑卡片json
* @prop {*[]} [head] - 非kv模式表头
* @prop {*[]} [data] - 非kv模式数据
* @prop {*[]} [use] - 插件,如果带data,则需带插件,否则附件加载有问题
*/ /** @type {Opt} */ const def = {
domProp: 'wiaEditTable',
tb: null,
el: null,
editTag: [
'input',
'textarea'
],
edit: false,
newEdit: false,
add: false,
kv: false,
labelWidth: 10,
col: 8,
// colWidth: [0.1, 0.15, 0.1, 0.15, 0.1, 0.15, 0.1, 0.15],
colRatio: 1,
name: null,
head: null,
data: null
};
/**
* @enum {number} 数据类型-页面呈现方式
*/ const DataType = {
null: 0,
text: 1,
texts: 2,
number: 3,
date: 4,
bool: 5,
select: 6,
radio: 7,
checkbox: 8,
chip: 9,
button: 10,
img: 11,
file: 12,
path: 13,
url: 14,
email: 15,
tel: 16,
password: 17,
time: 18,
datetime: 19,
month: 20,
week: 21,
color: 22,
attach: 23,
table: 24,
view: 25,
page: 26,
search: 27,
html: 28,
json: 29
};
/**
* @enum {string} 数据类型-页面呈现方式
*/ const DataTypes = {
null: 'null',
text: 'text',
texts: 'texts',
number: 'number',
date: 'date',
bool: 'bool',
select: 'select',
radio: 'radio',
checkbox: 'checkbox',
chip: 'chip',
button: 'button',
img: 'img',
file: 'file',
path: 'path',
url: 'url',
email: 'email',
tel: 'tel',
password: 'password',
time: 'time',
datetime: 'datetime',
month: 'month',
week: 'week',
color: 'color',
attach: 'attach',
table: 'table',
view: 'view',
page: 'page',
search: 'search',
html: 'html',
json: 'json'
};
/**
* @enum {number} 状态
*/ const State = {
null: 0,
view: 1,
edit: 2,
json: 3
};
/**
* EditTable
*/ let EditTable = class EditTable extends Event {
/**
* 构造函数
* @param {Page} page Page 实例
* @param {Opts} opts
*/ constructor(page, opts){
super(opts, [
page
]), /** @type {*} */ this.editTx = null // 当前被编辑的目标对象
, /** @type {boolean} */ this._keyDown = false // 记录键盘被按下的状态,当有键盘按下时其值为true
, /** @type {*} */ this._selRow = null // 最近的选择行
, /** @type {*} */ this._editCell = null // 当前编辑对象
, /** @type {*} */ this.editRow = null // 最后编辑行
, this.editCursorPos = 0 // 最后编辑光标位置
, this.rowNum = 0 // 表行数
, this.newNum = 0 // 新增行数
, this.dataid = 0 // 当前数据索引,用于render row
, /** @type {State} */ this.state = State.null, this.sel = new Set() // 选择
, this.add = new Set() // 新增
, this.del = new Set() // 删除
, this.uses = new Set() // 插件
;
const _ = this;
_.page = page;
const opt = {
...def,
...opts
};
_.opt = opt;
const { el, tb: tb1, kv, head, data } = opt;
// 是否为kv模式,非kv需带表头
if (kv && tb1) {
_.tb = tb1;
// 克隆数组数据,操作时,不改变原数据
if (data?.length) _.data = [
...data
];
} else if (head && el) {
_.head = opt.head;
const cfg = {
...opt.head[0] || {}
};
if (cfg.id) cfg.checkbox = cfg.id;
else cfg.checkbox = 'index';
_.cfg = cfg;
const fields = _.head.slice(1);
// 创建 field 的深拷贝,避免修改原配置对象value后,导致原列表出问题
_.fields = fields.map((f)=>({
...f
}));
_.lastW = window.innerWidth;
_.lastH = window.innerHeight;
// 已创建,直接返回
if (el.dom[opt.domProp]) {
const instance = el.dom[opt.domProp];
_.destroy();
return instance;
}
el.dom[opt.domProp] = this;
// 容器
_.el = el;
// 克隆数组数据,操作时,不改变原数据
if (data) _.data = [
...data
];
}
// 4 改为 8,兼容旧模式
if (!opt.colWidth) {
opt.colRatio = 2;
opt.col = opt.col * 2;
if (opt.col === 8) opt.colWidth = [
0.1,
0.15,
0.1,
0.15,
0.1,
0.15,
0.1,
0.15
];
}
_.init();
if (opt.edit) _.edit();
else _.view();
_.bind();
// const txs = $(tr).find('input.etCellView')
// const spans = $(tr).find('span.etLabel')
// spans.html('hello')
// for (const tx of txs.get()) {
// const $tx = $(tx)
// $tx.click(ev => editSpec(tx))
// $tx.focus(ev => editSpec(tx))
// $tx.blur(ev => viewSpec(tx))
// $tx.upper('td').addClass('border-bot')
// }
}
static hi(msg) {
alert(msg);
}
/**
* kv模式,构建空表头、表体
*/ init() {
const _ = this;
const { opt, tb: tb1 } = _;
const { colWidth, edit, kv, use } = opt;
try {
// 加载数据之前,先加载插件
for (const u of use || [])if (u.cls) _.use(u.cls, u.opts);
if (kv) {
// 列宽控制
const cg = tb1.find('colgroup');
if (!cg.dom && colWidth?.length) {
tb1.prepend(/*#__PURE__*/ _jsx("colgroup", {
children: colWidth.map((v)=>{
let width;
if (v < 1) width = `${v * 100}%`;
else width = `${v}px`;
return /*#__PURE__*/ _jsx("col", {
style: `width: ${width}`
});
})
}));
}
// 构造空body
let body = tb1.find('tbody');
if (!body.dom) {
if (edit) tb1.append(/*#__PURE__*/ _jsx("tbody", {
class: "etEdit"
}));
else tb1.append(/*#__PURE__*/ _jsx("tbody", {
class: "etView"
}));
}
body = tb1.find('tbody');
// 构造空表头
const th = tb1.find('thead');
if (!th.dom) {
body.before(/*#__PURE__*/ _jsx("thead", {
children: /*#__PURE__*/ _jsx("tr", {
style: "display: none",
class: "etRowOdd"
})
}));
}
// 数据视图
if (_.data?.length) _.setKv();
} else _.render();
} catch (e) {
log.err(e, 'init');
}
}
/**
* 生成edit table,包括 thead、tbody
* @returns
*/ render() {
const _ = this;
try {
const { el, opt, cfg, fields } = _;
const { edit } = opt;
const head = [
cfg,
...fields
];
if (!head) {
console.log('param is null.');
return;
}
// checkbox
const { checkbox: ck, layout, sum, fix } = cfg;
if (fix.includes('table')) // 固定表格,上下滚动
el.append(/*#__PURE__*/ _jsx("div", {
class: "data-table-content overflow-auto"
}));
else el.append(/*#__PURE__*/ _jsx("div", {
class: "data-table-content"
}));
let ckv = '';
if (ck) {
// checkbox
if (Array.isArray(ck) && ck.length) {
ckv = `\${r[${ck[0]}]}`;
} else if (ck === 'index') ckv = '${r.index}';
}
const { name } = opt;
// 默认固定表头、表尾
const clas = [
'edit-table',
'fix-h',
'fix-b'
];
if (fix.includes('right1')) // 固定表头 表尾
clas.push('fix-r1');
if (fix.includes('right2')) clas.push('fix-r2');
if (fix.includes('left1')) clas.push('fix-l1');
if (fix.includes('left2')) clas.push('fix-l2');
if (fix.includes('left3')) clas.push('fix-l3');
if (fix.includes('left4')) clas.push('fix-l4');
if (fix.includes('left5')) clas.push('fix-l5');
const style = [
`table-layout: ${layout}`
];
const tb1 = $(/*#__PURE__*/ _jsx("table", {
name: name,
class: clas.join(' '),
style: style.join(';')
}));
// 保存tb
_.tb = tb1;
// 加入到容器
const tbWrap = el.findNode('.data-table-content');
tbWrap.append(tb1);
// 列宽
tb1.append(/*#__PURE__*/ _jsx("colgroup", {
children: col(head)
}));
// <table name="tbLoan">
// jsx 通过函数调用,实现html生成。
let v = th(head, false);
// 加入到表格
tb1.append(v);
const thead = tb1.tag('THEAD');
thead.append(/*#__PURE__*/ _jsx("tr", {
name: `${name}-tp`,
style: "display: none",
children: ck && /*#__PURE__*/ _jsx("td", {
class: "checkbox-cell",
children: /*#__PURE__*/ _jsxs("label", {
class: "checkbox",
children: [
/*#__PURE__*/ _jsx("input", {
type: "checkbox",
"data-val": ckv
}),
/*#__PURE__*/ _jsx("i", {
class: "icon-checkbox"
})
]
})
})
}));
// 表主体
v = /*#__PURE__*/ _jsx("tbody", {
name: "tbBody",
class: `${edit ? 'etEdit' : 'etView'}`
});
// 加入到表格
tb1.append(v);
_.header = el.findNode('.data-table-header');
_.$headerSel = el.findNode('.data-table-header-selected');
// 数据视图
if (_.data?.length) _.setView();
} catch (ex) {
console.log('render', {
ex: ex.message
});
}
}
bind() {
const _ = this;
const { opt, fields, vals } = _;
const { kv } = opt;
// 表格点击事件
// 编辑元素(input) 不能 focus,不能 onblur?原因:pointer-events: none
_.tb.click(async (ev)=>{
// 阻止冒泡,否则会莫名其妙的(内嵌表格编辑叠加到kv编辑,导致混乱)事件!
// ev.preventDefault()
ev.stopPropagation();
const $ev = $(ev);
const th = $ev.upper('th');
if (th?.length) return;
if (_.state !== State.edit) return;
// 点击 input、select 则跳过
if ([
'SELECT',
'INPUT'
].includes(ev.target.tagName)) return;
const td = $ev.upper('td');
let span = $ev.upper('span');
span = td.find('span');
if (span.eq(0).css('display') === 'none') {
// debugger
return;
}
const idx = td?.data('idx') // 数据或字段索引
;
const idy = td?.data('idy') // 表编辑,多行数据行索引
;
const idv = td?.data('idv') // 数据中的value索引,多值数组模式下
;
const value = td?.attr('data-value') // 数据原值 data() 会自动转换 json 字符串
;
const r = fields?.[idx];
if (r) {
if (r.read) return; // 只读
// 方法2.2(更可靠)
// document.body.setAttribute('tabindex', '-1')
// document.body.focus()
// document.body.removeAttribute('tabindex')
let type = r.type ?? DataType.text // 默认单行字符串
;
if (type === 'string') type = DataType.text;
// 多值
if (idv && idv >= 0 && Array.isArray(type) && type[idv]) {
type = type[idv];
}
const inputType = _.getInputType(type);
if (inputType) {
const span = td.find('span');
span.hide();
let tx = td.find('input');
if (!tx.dom) {
tx = document.createElement('input');
tx.name = r.field;
tx.type = inputType;
td.append(tx);
tx = $(tx);
tx.addClass('dy-input');
tx.blur((ev)=>{
// _.viewCell()
const val = tx.val();
span.eq(0).html(val);
// 比较值是否被修改
if (`${val}` === `${value}`) {
tx.hide();
span.show();
td.removeClass('etChange');
} else {
vals[idy][r.field] = val;
td.addClass('etChange');
}
});
}
tx.val(span.eq(0).html());
tx.show();
tx.focus();
// 自动聚焦到输入框
// setTimeout(() => {
// tx.focus()
// }, 50)
} else if (type === DataType.texts || type === DataTypes.texts) {
const span = td.find('span');
if (!span.hasClass('edit')) {
let tx = td.find('input');
if (!tx.dom) {
tx = document.createElement('input');
tx.name = r.field;
tx.value = span.html();
tx.hidden = true;
td.append(tx);
}
span.dom.tabIndex = '-1';
// span 可编辑
// span.focus(ev => span.addClass('edit'))
span.addClass('edit');
span.blur((ev)=>{
// _.viewCell()
const val = span.html();
tx.value = val;
if (`${val}` === `${value}`) {
span.removeClass('edit') // span 可编辑
;
td.removeClass('etChange');
} else {
vals[idy][r.field] = val;
td.addClass('etChange');
}
});
span.focus();
// span.dom.addEventListener('focusout', ev => {
// tx.value = span.html()
// span.removeClass('edit') // span 可编辑
// })
}
} else if (type === DataType.select || type === DataTypes.select) {
const span = td.find('span');
span.hide();
let key;
let sel = td.find('select');
// 第一次创建
if (!sel.dom) {
sel = document.createElement('select');
sel.name = r.field;
td.append(sel);
sel = $(sel);
sel.addClass('dy-select dy-select-primary');
sel.click((ev)=>ev.stopPropagation()) // 阻止事件冒泡 tb 无感知?
;
// tx.addClass('dy-input')
// tx.val(span.html())
// tx.change(ev => {
sel.blur((ev)=>{
// _.viewCell()
let val;
const { option } = r;
if (Array.isArray(option)) val = sel.val();
else if (typeof option === 'object') {
key = sel.val();
val = option[key];
}
if (`${val}` === `${value}` || val === '') {
sel.hide();
span.html(value) // 还原值
;
span.show();
td.removeClass('etChange');
} else {
span.html(val) // 修改值
;
vals[idy][r.field] = val;
td.addClass('etChange');
}
});
sel.focus((ev)=>{
// 关联参数发生编号,重新查询
_.fillOption(r, td, sel, value, idy);
});
}
// 选项
await _.fillOption(r, td, sel, value, idy);
sel.show();
sel.focus();
// 弹出下拉列表,基本无效!
// 等待一帧,确保已渲染
requestAnimationFrame(()=>{
// 尝试用鼠标事件打开(Chromium 下通常有效)
const ev = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
view: window
});
sel.dom.dispatchEvent(ev);
// 如果还不行,退一步触发键盘组合(某些浏览器用 Alt+↓ 打开)
if (document.activeElement === sel) {
const keyEv = new KeyboardEvent('keydown', {
key: 'ArrowDown',
altKey: true,
bubbles: true
});
sel.dom.dispatchEvent(keyEv);
}
});
} else if ((type === DataType.search || type === DataTypes.search) && _.Autocomplete) {
const span = td.find('span');
// 切换到编辑模式
if (span.css('display') !== 'none') {
span.hide();
let dvAc = td.find('.autocomplete');
let ac = dvAc.dom?._wiaAutocomplete;
// 创建Ac
if (!ac) {
const { source, field, addUrl } = r;
const { placeholder } = r;
// td.append()
dvAc = $(/*#__PURE__*/ _jsx("div", {
class: "autocomplete"
})).appendTo(td);
// 保存当前字段 search回来的数据,表编辑时 其它行共享
if (!r.option) r.option = [];
// 处理 source.param 中的 ${} 引用(与 select 组件保持一致)
let processedSource = source;
if (source && typeof source === 'object' && source.param) {
let { param = {} } = source;
// 查询参数 可引用其他字段值
param = _.parseRef(param, idy);
processedSource = {
...source,
param
};
}
// r.option = option // 不保存到字段定义,避免污染
// tx.addClass('dy-input')
ac = new _.Autocomplete(_.page, {
el: dvAc,
data: r.option,
name: field,
value,
placeholder,
// refEl: [span.dom], // 点击该关联元素不关闭下拉列表,点击其他地方,关闭列表
source: processedSource,
addUrl
});
ac.on('blur', ()=>{
// 选择赋值在 blur 后
setTimeout(()=>{
const val = ac.val() //tx.val()
;
if (`${val}` === `${value}` || val === '') {
ac.hide();
span.eq(0).html(value) // 还原值
;
span.show();
td.removeClass('etChange');
} else {
span.eq(0).html(val) // 修改值
;
vals[idy][r.field] = val;
td.addClass('etChange');
}
}, 200);
});
}
ac.show();
ac.focus() // 自动触发下拉
;
}
} else if (type === DataType.bool || type === DataTypes.bool) {
const span = td.find('span');
span.hide();
let val = span.html();
let key;
let tx = td.find('select');
if (!tx.dom) {
tx = document.createElement('select');
tx.name = r.field;
td.append(tx);
tx = $(tx);
tx.addClass('dy-select dy-select-primary');
const option = {
true: '是',
false: '否'
};
// 添加选项
const htm = [];
for (const k of Object.keys(option)){
const v = option[k];
if (v === val) {
key = k;
htm.push(/*#__PURE__*/ _jsx("option", {
selected: true,
value: k,
children: v
}));
} else htm.push(/*#__PURE__*/ _jsx("option", {
value: k,
children: v
}));
}
tx.html(htm.join(''));
if (key) tx.val(key);
else tx.val(val);
tx.click((ev)=>ev.stopPropagation()) // 阻止事件冒泡
;
// tx.addClass('dy-input')
// tx.val(span.html())
tx.blur((ev)=>{
// _.viewCell()
key = tx.val();
val = option[key];
span.html(val);
if (`${val}` === `${value}`) {
tx.hide();
span.show();
td.removeClass('etChange');
} else {
vals[idy][r.field] = val;
td.addClass('etChange');
}
});
}
tx.show();
tx.focus();
setTimeout(()=>{
// 创建并触发鼠标事件来展开下拉框
// const event = new MouseEvent('mousedown')
const event = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
view: window
});
tx.dom.dispatchEvent(event);
}, 100);
// tx.click()
} else if (type === DataType.url || type === DataTypes.url) {
// urlChange
const span = td.find('span');
span.hide();
let tx = td.find('input');
if (!tx.dom) {
tx = document.createElement('input');
tx.name = r.field;
tx.type = 'url';
td.append(tx);
tx = $(tx);
tx.addClass('dy-input');
tx.blur((ev)=>{
// _.viewCell()
const val = tx.val();
span.eq(0).find('a').attr('href', val);
// 比较值是否被修改
if (`${val}` === `${value}`) {
tx.hide();
span.show();
td.removeClass('etChange');
} else {
vals[idy][r.field] = val;
td.addClass('etChange');
}
});
}
// urlChange
tx.val(span.eq(0).find('a').attr('href'));
tx.show();
tx.focus();
}
}
// for (const tag of _.opt.editTag) {
// const tx = $ev.upper(tag)
// if (tx.dom) {
// _.editCell(tx.dom)
// break
// }
// }
});
// 处理checkbox
_.tb.on('change', '.checkbox-cell input[type="checkbox"]', function(ev) {
_.handleCheck(ev, this);
});
_.tb.on('change', '.etCheckbox input[type="checkbox"]', (ev)=>{
const td = $(ev).upper('td');
const idx = td?.data('idx') // 数据或字段索引
;
const r = fields?.[idx];
const { type } = r;
if ([
DataType.checkbox,
DataTypes.checkbox
].includes(type)) {
const { val, value } = _.getVal(td)?.[0] || {};
if (JSON.stringify(val) !== JSON.stringify(value)) td.addClass('etChange');
else td.removeClass('etChange');
}
});
_.tb.on('change', '.etRadio input[type="radio"]', (ev)=>{
const td = $(ev).upper('td');
const idx = td?.data('idx') // 数据或字段索引
;
const r = fields?.[idx];
const { type } = r;
if ([
DataType.radio,
DataTypes.radio
].includes(type)) {
const { val, value } = _.getVal(td)?.[0] || {};
if (JSON.stringify(val) !== JSON.stringify(value)) td.addClass('etChange');
else td.removeClass('etChange');
}
});
}
/**
* checkbox Events
* @param {*} ev
* @param {HTMLElement} el
* @returns
*/ handleCheck(ev, el) {
const _ = this;
try {
// 代码更改checkbox属性,不会触发change,代码触发change,这里需排除,避免循环
if (ev.detail && ev.detail.sentByWiaF7Table) {
// Scripted event, don't do anything
return;
}
const $el = $(el);
// 是否被选中
const { checked } = el;
const val = $el.data('val');
if (val != null) {
if (checked) _.sel.add(val);
else _.sel.delete(val);
}
// 列数
const columnIndex = $el.parents('td,th').index();
// 表体checkbox
if (columnIndex === 0) $el.parents('tr')[checked ? 'addClass' : 'removeClass']('data-table-row-selected');
// _.headerCheck(columnIndex)
// 延迟到change事件后触发,避免统计选择行数据差错
_.handleSel();
} catch {}
}
/**
* 表头选择区域显示切换,统计选择行,触发选择改变事件,方便跨页统计
*/ handleSel() {
const _ = this;
try {
const { el } = _;
// 选中行
const rs = el.find('.data-table-row-selected');
const len = rs.length;
// 改变表头操作面板
const hd = el.find('.data-table-header');
const hdSel = el.find('.data-table-header-selected');
if (hd.length && hdSel.length) {
if (len && !el.hasClass('data-table-has-checked')) el.addClass('data-table-has-checked');
else if (!len && el.hasClass('data-table-has-checked')) el.removeClass('data-table-has-checked');
// 选中数量,跨行选择数量与当前也选择数量不一致
hdSel.find('.data-table-selected-count').text(len);
}
// 触发当前表选择事件,参数为选择行
// 延迟到change事件后触发,避免跨页统计选择行数据差错
setTimeout(()=>{
_.emit('local::select', rs);
}, 10);
} catch {}
}
/**
* 选择表所有行,包括跨页
* 表头checkbox只能选择当前页面所有行
*/ cancelSel() {
try {
this.clearSel();
// 每行checkbox
// this.headerCheck()
// 表头选择区
this.handleSel();
} catch (e) {
log.err(e, 'cancelSel');
}
}
/**
* 清除选择(包括跨页),切换表头区及表头checkbox状态
*/ clearSel() {
const _ = this;
try {
const { el } = _;
if (_.sel?.size) _.sel.clear();
// 切换表头为非选择模式
el?.removeClass('data-table-has-checked');
const rs = el.find('.data-table-row-selected');
if (rs.length) rs.removeClass('data-table-row-selected');
// 更新header的checkbox
// const col = 0
// const ckb = this.el.findNode(`thead .checkbox-cell:nth-child(${col + 1}) input[type="checkbox"]`)
// ckb.prop('indeterminate', false) // 部分选中
const cks = el.find(`.checkbox-cell input[type="checkbox"]`);
if (cks.length) cks.prop('checked', false);
} catch (e) {
log.err(e, 'clearSel');
}
}
/**
* 填充select option
* 每次点击下拉时,检查关联参数是否变化,变化则重新获取选项
* 根据选项,重新生成select 内容,不缓存
* @param {*} r - 字段定义
* @param {Dom} td
* @param {Dom} sel - select
* @param {string} value - 原数据
* @param {number} idy - 表数据行索引
* @returns
*/ async fillOption(r, td, sel, value, idy = 0) {
const _ = this;
try {
const { source } = r;
let { param = {} } = source || {};
// 查询参数 可引用其他字段值
param = _.parseRef(param, idy);
// 引用字段值是否变化
let change;
const curParam = JSON.stringify(param);
const lastParam = r.lastSourceParam;
// if (typeof lastParam !== 'string') lastParam = JSON.stringify(lastParam)
if (curParam !== lastParam) {
change = true;
r.lastSourceParam = curParam // 保存查询参数,避免重复查询
;
}
let { option } = r;
let { name } = param;
// if (name?.includes('${')) {
// const lastRefField = td.data('lastRefField') // 保存关联
// const match = name.match(/\$\{([^}]+)\}/)
// const ref = match?.[1]
// const i = _.getDataIdx({field: ref})
// if (i) {
// // 关联节点
// const n = _.tb.findNode(`[data-idx="${i}"]`)
// const v = n.findNode('span').html()
// if (v && v !== lastRefField) {
// change = true
// td.data('lastRefField', v) // 保存关联
// // 替换 'city:${province}'
// name = name.replace(`\${${ref}}`, v)
// }
// }
// }
// 关联字段变化或无选项,动态获取
if (source && (change || !option?.length)) {
sel.html('');
// 数据字典查询
// 默认 name = field
if (!name) name = r.field;
// source.param.name = name
option = await getOption(source, name);
r.option = option // 保存选项到字段定义,避免重复查询
;
let cnt = 0;
if (Array.isArray(option)) cnt = option.length;
else if (typeof option === 'object') cnt = Object.keys(option).length;
log({
source,
name,
cnt
}, 'fillOption.getOption');
}
// 插入当前值,保存后需清除
if (!option && value) option = [
value
];
if (option) {
let key;
const span = td.find('span');
const val = span.html() // 当前显示值
;
// 添加选项
let htm = [];
if (Array.isArray(option)) {
if (!option.includes(value)) option.unshift(value) // 加入原始值
;
htm = option.map((v)=>{
let rt;
if (v === val) rt = /*#__PURE__*/ _jsx("option", {
selected: true,
value: v,
children: v
});
else rt = /*#__PURE__*/ _jsx("option", {
value: v,
children: v
});
return rt;
});
} else if (typeof option === 'object') {
const has = Object.values(option).some((v)=>`${v}` === `${value}`);
if (!has) option[value] = value // 加入原始值
;
if (!val) {
htm.push(/*#__PURE__*/ _jsx("option", {
selected: true,
value: "",
children: "请选择"
}));
}
for (const k of Object.keys(option)){
const v = option[k];
if (v === val) {
key = k;
htm.push(/*#__PURE__*/ _jsx("option", {
selected: true,
value: k,
children: v
}));
} else htm.push(/*#__PURE__*/ _jsx("option", {
value: k,
children: v
}));
}
}
// r.option = option // 不保存到字段定义,避免污染
sel.html(htm.join(''));
if (key) sel.val(key);
else sel.val(val);
}
} catch (e) {
log.err(e, 'fillOption');
}
}
clearOption() {}
/**
* 解析引用字段
* @param {*} src
* @param {number} [idy] - 表数据行索引
* @param {*} [fv] - 字段值,外部传入可加快速度
* @returns
*/ /**
* 递归检查对象中是否包含 ${} 引用
* @param {*} obj 要检查的对象
* @returns {boolean} 是否包含引用
*/ hasRef(obj) {
// 处理 null/undefined
if (obj == null) return false;
// 字符串类型:直接检查
if (typeof obj === 'string') {
return /\$\{[^}]*\}/.test(obj);
}
// 对象类型(排除数组):递归检查
if (typeof obj === 'object' && !Array.isArray(obj)) {
for (const k of Object.keys(obj)){
if (this.hasRef(obj[k])) return true;
}
}
return false;
}
/**
* 递归解析对象中的 ${} 引用(直接修改原对象)
* @param {*} obj 要解析的对象
* @param {*} fv 字段值对象
* @returns {*} 解析后的值或对象
*/ parseRefRecursive(obj, fv) {
// 处理 null/undefined:直接返回
if (obj == null) return obj;
// 字符串类型:检查并解析
if (typeof obj === 'string') {
if (/\$\{[^}]*\}/.test(obj)) {
const val = Function('r', `return \`${obj}\``)(fv);
log({
src: obj,
val
}, 'parseRef');
return val;
}
return obj;
}
// 对象类型(排除数组):递归解析每个属性
if (typeof obj === 'object' && !Array.isArray(obj)) {
for (const k of Object.keys(obj)){
obj[k] = this.parseRefRecursive(obj[k], fv);
}
return obj;
}
// 其他类型(数组、数字等):直接返回
return obj;
}
parseRef(src, idy = 0, fv = null) {
let R = src;
const _ = this;
try {
const { data, opt, vals, fields } = _;
const { kv } = opt;
// 使用递归方法检查是否包含引用
const ref = _.hasRef(src);
if (ref) {
if (!fv) {
/** @type {*} */ fv = {} // 获取当前行最新数据
;
if (kv) {
for (const d of data){
const { field, type } = d;
fv[field] = vals[0][field] ?? d.value;
if ([
'number',
DataType.number
].includes(type) && isNumber(fv[field])) fv[field] = Number(fv[field]);
}
} else {
for (const f of fields){
const { field, idx, type } = f;
const val = data[idy][idx];
fv[field] = vals[idy][field] ?? val;
if ([
'number',
DataType.number
].includes(type) && isNumber(fv[field])) fv[field] = Number(fv[field]);
}
}
}
// 使用递归方法解析引用(直接修改原对象)
if (typeof src === 'object' && !Array.isArray(src)) {
_.parseRefRecursive(src, fv);
R = src;
} else {
const val = Function('r', `return \`${src}\``)(fv);
R = val;
log({
idy,
src,
val
}, 'parseRef');
}
}
} catch (e) {
log.err(e, 'parseRef');
}
return R;
}
/**
* 获得数据索引
* @param {*} opts
* @returns {number}
*/ getDataIdx(opts) {
let R;
const _ = this;
try {
const { field } = opts;
if (field) {
const idx = _.data.findIndex((v)=>v.field === field);
if (idx >= 0) R = idx;
}
} catch (e) {
log.err(e, 'getData');
}
return R;
}
/**
* 加载插件
* @param {*} cls
* @param {*} [opts]
*/ use(cls, opts) {
const _ = this;
try {
const { opt } = _;
_[cls.name] = cls;
_.uses.add({
cls,
opts
});
if (cls.name === 'Uploader' && opts?.upload) opt.upload = opts.upload;
else if (cls.name === 'Tabulate' && (opts?.getSelAll || opts?.saveEmbTb)) {
opt.getSelAll = opts.getSelAll;
opt.saveEmbTb = opts.saveEmbTb;
opt.prjid = opts.prjid;
opt.upload = opts.upload;
} else if (cls.name === 'JsonView' && opts?.updateJson) opt.updateJson = opts.updateJson;
} catch (e) {
log.err(e, 'use');
}
}
/**
* input type
* @param {DataType|DataTypes} type
*/ getInputType(type) {
let R;
switch(type){
case DataTypes.text:
case DataType.text:
R = 'text';
break;
case DataTypes.number:
case DataType.number:
R = 'text';
break;
case DataTypes.date:
case DataType.date:
R = 'date';
break;
// urlChange
// case DataTypes.url:
// case DataType.url:
// R = 'url'
// break
case DataTypes.email:
case DataType.email:
R = 'email';
break;
case DataTypes.tel:
case DataType.tel:
R = 'tel';
break;
case DataTypes.password:
case DataType.password:
R = 'password';
break;
case DataTypes.time:
case DataType.time:
R = 'time';
break;
case DataTypes.datetime:
case DataType.datetime:
R = 'datetime-local';
break;
case DataTypes.month:
case DataType.month:
R = 'month';
break;
case DataTypes.week:
case DataType.week:
R = 'week';
break;
case DataTypes.color:
case DataType.color:
R = 'color';
break;
}
return R;
}
/*
// 异步存储
set(key, data, expires) {
_storage.save({
key: key, // Note: Do not use underscore("_") in key!
data: data,
expires: expires
});
}
// Promise同步方法
getStore(key) {
// load
_storage.load({
key: key,
autoSync: true,
syncInBackground: true
}).then(data => {
return data;
}).catch(err => {
console.warn(err);
return null;
})
}
// async 的写法
async get(key) {
try {
let data = await storage.load({
key: key
});
return data;
}
catch (err) {
console.warn(err);
return null;
}
}
*/ // Chrome、Safari 阻止浏览器的默认事件,实现 全选
mouseupCell(evt) {
const ev = evt || window.event;
ev.preventDefault();
}
addHandler() {
const data = this.tabulate.getData();
this.tabulate.addRow();
}
saveTable() {
this.tabulate.saveTable();
}
/**
* 初始化表格编辑器
*/ async editModeTable() {
const _ = this;
try {
const { page, opt, data } = _;
const tds = _.tb.find('td[data-idx]');
console.log(tds, 'tds');
//! 应该根据field 创建,支持多个内嵌表格编辑
//! 需判断是否已创建,避免重复创建
//! 应该根据字段类型(内嵌表)创建,而不是在编辑模式没有内嵌表也创建
// if (_.hasTable && !_.tabulate && _.Tabulate) {
if (_.hasTable) {
if (opt.newEdit) {
const dvs = _.tb.find('div.data-table');
for (const dv of dvs){
const $dv = $(dv);
const name = $dv.attr('name');
const { wiaDataTable: dtb, wiaEditTable: etb } = dv;
if (!etb) {
const { head } = dtb;
const { api } = head[0];
if (api.param) api.param = _.parseRef(api.param);
dv.wiaEditTable = makeEdit(page, {
el: $dv,
name,
head,
data: dtb.data,
use: [
..._.uses
]
});
} else {
etb.show();
dtb.hide();
}
}
} else {
_.tabulate = new _.Tabulate({
containerName: 't-table',
addButtonName: 'add-button',
targetBox: _.tb.tag('tbody')[0].querySelectorAll('.data-table'),
baseTableInfo: _.baseTableInfo,
getSelAll: opt.getSelAll,
saveEmbTb: opt.saveEmbTb,
viewid: opt.viewid,
prjid: opt.prjid,
upload: opt.upload
});
}
} else {
const tTableDivs = new Set();
const dataTables = _.tb.tag('tbody')[0].querySelectorAll('.data-table');
dataTables.forEach((table)=>{
table.style.display = 'none';
// 获取当前 .data-table 的父节点(兄弟元素的共同容器)
const parent = table.parentNode;
// 在父节点中查找 name="t-table" 的 div
const tTableDiv = parent.querySelector('div[name="t-table"]');
const addButton = parent.querySelector('button[name="add-button"]');
// 找到后添加到集合(去重,避免重复元素)
if (tTableDiv) {
tTableDiv.style.display = 'block';
addButton.style.display = 'block';
tTableDivs.add(tTableDiv);
} else {
_.tabulate.destroyTabulateInstance(_.tabulate);
_.tabulate = new _.Tabulate({
containerName: 't-table',
addButtonName: 'add-button',
targetBox: _.tb.tag('tbody')[0].querySelectorAll('.data-table'),
baseTableInfo: _.baseTableInfo,
getSelAll: opt.getSelAll,
saveEmbTb: opt.saveEmbTb,
viewid: opt.viewid,
prjid: opt.prjid,
upload: opt.upload
});
}
});
}
} catch (e) {}
}
/**
* 编辑模式
*/ edit() {
try {
const _ = this;
if (_.state === State.json) {
_.tb.parent().find('.json-view-box').hide();
_.tb.show();
}
_.state = State.edit;
_.tb.tag('tbody').addClass('etEdit').removeClass('etView');
_.editModeTable();
editAttach(_.tb);
chip.edit(_.tb);
// 更新所有 CatAttach 实例的编辑状态和图集模式
const catAttachTds = _.tb.find('td[catAttachField]');
for (const td of catAttachTds){
const $td = $(td);
const instance = $td.data('catAttach');
if (instance) {
// 进入编辑模式时,先切换到图集模式
if (typeof instance.setAllToGridMode === 'function') {
instance.setAllToGridMode();
}
// 然后设置编辑状态
if (typeof instance.setEditMode === 'function') {
instance.setEditMode(true);
}
}
}
// _.bind()
} catch (e) {
log.err(e, 'edit');
}
}
/**
* @param {{ etb: EditTable; dtb: any; path: string; no: string; tab?: string; name: string; icon?: string; card?: HTMLElement; tb?: JQuery; data: any[]; }} data
*/ json(data) {
const _ =