@tmagic/form 1.8.0-beta.11 → 1.8.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,408 @@
1
+ import { readonly } from "vue";
2
+ import { appendValidateSuggestion } from "@tmagic/design";
3
+ import { getValueByKeyPath, toLine } from "@tmagic/utils";
4
+ import dayjs from "dayjs";
5
+ import customParseFormat from "dayjs/plugin/customParseFormat";
6
+ //#region packages/form/src/utils/typeMatch.ts
7
+ dayjs.extend(customParseFormat);
8
+ var typeMatchRuleRegistry = /* @__PURE__ */ new Map();
9
+ var isPromise = (value) => typeof value === "object" && value !== null && typeof value.then === "function";
10
+ /** 注册或覆盖某个字段 type 的 typeMatch 校验规则 */
11
+ var registerTypeMatchRule = (type, validator) => {
12
+ typeMatchRuleRegistry.set(toLine(type), validator);
13
+ };
14
+ /** 批量注册 typeMatch 校验规则 */
15
+ var registerTypeMatchRules = (rules) => {
16
+ Object.entries(rules).forEach(([type, validator]) => {
17
+ registerTypeMatchRule(type, validator);
18
+ });
19
+ };
20
+ /** 获取某个字段 type 的自定义 typeMatch 校验规则 */
21
+ var getTypeMatchRule = (type) => typeMatchRuleRegistry.get(toLine(type));
22
+ /** 删除某个字段 type 的自定义 typeMatch 校验规则 */
23
+ var deleteTypeMatchRule = (type) => typeMatchRuleRegistry.delete(toLine(type));
24
+ /** 清空所有自定义 typeMatch 校验规则 */
25
+ var clearTypeMatchRules = () => {
26
+ typeMatchRuleRegistry.clear();
27
+ };
28
+ /** 本地解析配置函数,避免与 form.ts 循环依赖 */
29
+ var resolveConfig = (mForm, config, props) => {
30
+ if (typeof config === "function") return config(mForm, {
31
+ values: readonly(mForm?.initValues || {}),
32
+ model: readonly(props.model),
33
+ parent: readonly(mForm?.parentValues || {}),
34
+ formValue: readonly(mForm?.values || props.model),
35
+ prop: props.prop,
36
+ config: props.config,
37
+ index: props.index,
38
+ getFormValue: (prop) => getValueByKeyPath(prop, mForm?.values || props.model)
39
+ });
40
+ return config;
41
+ };
42
+ var SKIP_TYPES = /* @__PURE__ */ new Set([
43
+ "display",
44
+ "hidden",
45
+ "row",
46
+ "tab",
47
+ "dynamic-tab",
48
+ "fieldset",
49
+ "panel",
50
+ "step",
51
+ "flex-layout",
52
+ "link",
53
+ "component",
54
+ "dynamic-field",
55
+ "dynamicfield",
56
+ "form",
57
+ "container"
58
+ ]);
59
+ var STRING_TYPES = /* @__PURE__ */ new Set([
60
+ "text",
61
+ "textarea",
62
+ "color-picker",
63
+ "html",
64
+ ""
65
+ ]);
66
+ var DATE_LIKE_TYPES = /* @__PURE__ */ new Set([
67
+ "date",
68
+ "datetime",
69
+ "time"
70
+ ]);
71
+ var DATE_LIKE_DEFAULT_VALUE_FORMAT = {
72
+ date: "YYYY/MM/DD",
73
+ datetime: "YYYY/MM/DD HH:mm:ss",
74
+ time: "HH:mm:ss",
75
+ daterange: "YYYY/MM/DD HH:mm:ss",
76
+ timerange: "HH:mm:ss"
77
+ };
78
+ var TIMESTAMP_VALUE_FORMATS = /* @__PURE__ */ new Set(["x", "timestamp"]);
79
+ /**
80
+ * 将值格式化为可读的参考示例字符串。
81
+ */
82
+ var stringifyExampleValue = (value) => {
83
+ if (typeof value === "string") return `"${value}"`;
84
+ if (value === null || value === void 0) return String(value);
85
+ if (typeof value === "object") try {
86
+ return JSON.stringify(value);
87
+ } catch {
88
+ return String(value);
89
+ }
90
+ return String(value);
91
+ };
92
+ var MAX_SUGGESTION_OPTIONS = 5;
93
+ /**
94
+ * 生成「请使用以下某一个值:xxx;xxx」形式的参考建议;无可选值时返回空字符串(不追加建议)。
95
+ * 可选值超过 MAX_SUGGESTION_OPTIONS 个时仅展示前若干个并以「等」省略。
96
+ */
97
+ var optionSuggestion = (optionValues) => {
98
+ const values = optionValues.filter((item) => typeof item !== "undefined");
99
+ if (!values.length) return "";
100
+ const shown = values.slice(0, MAX_SUGGESTION_OPTIONS).map(stringifyExampleValue);
101
+ const suffix = values.length > MAX_SUGGESTION_OPTIONS ? " 等" : "";
102
+ return `请使用以下某一个值:${shown.join(";")}${suffix}`;
103
+ };
104
+ /**
105
+ * 基于真实 options 生成类型不匹配场景的「参考示例值」。
106
+ *
107
+ * - expectArray 为 true 时,取前若干个真实可选值组成数组示例;
108
+ * - 否则取第一个真实可选值作为标量示例。
109
+ * 无可用 options 时回退到通用示例。
110
+ */
111
+ var optionExampleSuggestion = (optionValues, fallbackExample, expectArray) => {
112
+ const values = optionValues.filter((item) => typeof item !== "undefined");
113
+ if (!values.length) return `请参考以下示例值:${fallbackExample}`;
114
+ return `请参考以下示例值:${expectArray ? stringifyExampleValue(values.slice(0, 2)) : stringifyExampleValue(values[0])}`;
115
+ };
116
+ /**
117
+ * 生成开关类字段的参考建议,列出合法的开关值。
118
+ */
119
+ var toggleSuggestion = (activeValue, inactiveValue) => `请使用以下某一个值:${stringifyExampleValue(activeValue)};${stringifyExampleValue(inactiveValue)}`;
120
+ /**
121
+ * 解析字段配置中的真实默认值(defaultValue),用于生成「真实」的参考示例值。
122
+ * defaultValue 可能是函数,交由 resolveConfig 处理;未配置时返回 undefined。
123
+ */
124
+ var resolveFieldDefaultValue = (mForm, props) => {
125
+ const { defaultValue } = props.config || {};
126
+ if (typeof defaultValue === "undefined" || defaultValue === "undefined") return void 0;
127
+ const resolvedDefaultValue = resolveConfig(mForm, defaultValue, props);
128
+ if (isPromise(resolvedDefaultValue)) return;
129
+ return resolvedDefaultValue;
130
+ };
131
+ var isNumberValue = (value) => typeof value === "number" && !Number.isNaN(value);
132
+ var isStringValue = (value) => typeof value === "string";
133
+ var isObjectValue = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
134
+ var isObjectArrayValue = (value) => Array.isArray(value) && value.every((item) => isObjectValue(item));
135
+ var isNumberRangeValue = (value) => Array.isArray(value) && value.length === 2 && value.every((item) => isNumberValue(item));
136
+ /**
137
+ * 生成类型不匹配场景的「参考示例值」。
138
+ *
139
+ * 优先使用字段配置的真实默认值(defaultValue,需符合期望类型);无可用默认值时回退到通用示例。
140
+ */
141
+ var typeExampleSuggestion = (mForm, props, fallbackExample, isValid) => {
142
+ const defaultValue = resolveFieldDefaultValue(mForm, props);
143
+ return `请参考以下示例值:${typeof defaultValue !== "undefined" && isValid(defaultValue) ? stringifyExampleValue(defaultValue) : fallbackExample}`;
144
+ };
145
+ /**
146
+ * 返回最终错误文案。
147
+ *
148
+ * - 传入了自定义 `message` 时,直接使用自定义文案(不追加建议);
149
+ * - 否则使用默认文案 `fallback`,并在其后拼接「参考建议」。
150
+ * 参考建议给出一个可参考的示例值(如可选项列表、示例数据),仅用于错误汇总展示;
151
+ * form-item 行内错误不展示建议(由 @tmagic/design FormItem 在渲染时截断建议)。
152
+ * 主文案与建议的拼接/截断统一复用 @tmagic/design 的 `appendValidateSuggestion`。
153
+ */
154
+ var defaultMessage = (message, fallback, suggestion) => {
155
+ if (message) return message;
156
+ return appendValidateSuggestion(fallback, suggestion);
157
+ };
158
+ var isEmptyValue = (value) => value === void 0 || value === null || value === "";
159
+ var isEmptyArray = (value) => Array.isArray(value) && value.length === 0;
160
+ var resolveDateValueFormat = (fieldType, config) => config.valueFormat || DATE_LIKE_DEFAULT_VALUE_FORMAT[fieldType] || "YYYY/MM/DD HH:mm:ss";
161
+ var isTimestampValueFormat = (valueFormat) => TIMESTAMP_VALUE_FORMATS.has(valueFormat);
162
+ var isValidDateValueByFormat = (value, valueFormat) => {
163
+ if (isTimestampValueFormat(valueFormat)) return typeof value === "number" && !Number.isNaN(value);
164
+ if (typeof value !== "string") return false;
165
+ return dayjs(value, valueFormat, true).isValid();
166
+ };
167
+ /**
168
+ * 生成单个日期类字段的参考示例值:时间戳格式给出当前时间戳,其余按 valueFormat 格式化当前时间。
169
+ */
170
+ var dateSuggestion = (valueFormat) => isTimestampValueFormat(valueFormat) ? `请参考以下示例值:${Date.now()}` : `请参考以下示例值:"${dayjs().format(valueFormat)}"`;
171
+ /**
172
+ * 生成日期范围类字段的参考示例值(长度为 2 的数组)。
173
+ */
174
+ var dateRangeSuggestion = (valueFormat) => {
175
+ if (isTimestampValueFormat(valueFormat)) return `请参考以下示例值:[${Date.now()}, ${Date.now()}]`;
176
+ const example = dayjs().format(valueFormat);
177
+ return `请参考以下示例值:["${example}", "${example}"]`;
178
+ };
179
+ var dateValueFormatErrorMessage = (message, valueFormat) => defaultMessage(message, isTimestampValueFormat(valueFormat) ? "值类型应为时间戳数字" : `值格式应为 ${valueFormat}`, dateSuggestion(valueFormat));
180
+ var resolveToggleValues = (config) => {
181
+ const filterIsNumber = config.filter === "number";
182
+ const { activeValue: configActiveValue, inactiveValue: configInactiveValue } = config;
183
+ let activeValue = configActiveValue;
184
+ let inactiveValue = configInactiveValue;
185
+ if (typeof activeValue === "undefined") activeValue = filterIsNumber ? 1 : true;
186
+ if (typeof inactiveValue === "undefined") inactiveValue = filterIsNumber ? 0 : false;
187
+ return {
188
+ activeValue,
189
+ inactiveValue
190
+ };
191
+ };
192
+ var flattenSelectOptions = (options) => {
193
+ const values = [];
194
+ options.forEach((option) => {
195
+ if (Array.isArray(option?.options)) {
196
+ option.options.forEach((child) => {
197
+ if (typeof child?.value !== "undefined") values.push(child.value);
198
+ });
199
+ return;
200
+ }
201
+ if (typeof option?.value !== "undefined") values.push(option.value);
202
+ });
203
+ return values;
204
+ };
205
+ var resolveOptions = (props) => {
206
+ const { options } = props.config || {};
207
+ return Array.isArray(options) ? options : [];
208
+ };
209
+ var includesOptionValue = (optionValues, value) => optionValues.some((item) => Object.is(item, value));
210
+ var collectCascaderLeafValues = (options, result = []) => {
211
+ options.forEach((option) => {
212
+ if (option.children?.length) collectCascaderLeafValues(option.children, result);
213
+ else if (typeof option.value !== "undefined") result.push(option.value);
214
+ });
215
+ return result;
216
+ };
217
+ var isValidCascaderPath = (options, path) => {
218
+ if (!path.length) return false;
219
+ let currentOptions = options;
220
+ for (let i = 0; i < path.length; i++) {
221
+ const node = currentOptions.find((option) => Object.is(option.value, path[i]));
222
+ if (!node) return false;
223
+ if (i === path.length - 1) return true;
224
+ currentOptions = node.children || [];
225
+ }
226
+ return false;
227
+ };
228
+ /**
229
+ * 取 cascader options 的第一条完整路径(从顶层到叶子的 value 数组)。
230
+ */
231
+ var firstCascaderPath = (options) => {
232
+ const path = [];
233
+ let current = options;
234
+ while (current?.length) {
235
+ const node = current[0];
236
+ if (typeof node.value === "undefined") break;
237
+ path.push(node.value);
238
+ current = node.children;
239
+ }
240
+ return path;
241
+ };
242
+ /**
243
+ * 基于真实 cascader options 生成类型不匹配场景的「参考示例值」。
244
+ *
245
+ * 依据 valueSeparator / multiple / emitPath 组织真实选中值的形态;无可用 options 时回退到通用示例。
246
+ */
247
+ var cascaderExampleSuggestion = (options, config, valueSeparator, fallbackExample) => {
248
+ const path = firstCascaderPath(options);
249
+ if (!path.length) return `请参考以下示例值:${fallbackExample}`;
250
+ const emitPath = config.emitPath !== false;
251
+ const multiple = Boolean(config.multiple);
252
+ const single = emitPath ? path : path[path.length - 1];
253
+ let example;
254
+ if (valueSeparator) example = path.join(valueSeparator);
255
+ else if (multiple) example = [single];
256
+ else example = single;
257
+ return `请参考以下示例值:${stringifyExampleValue(example)}`;
258
+ };
259
+ var validateSelectValue = (value, config, optionValues, message) => {
260
+ if (optionValues.length === 0) return;
261
+ if (config.allowCreate || config.remote) {
262
+ if (config.multiple) {
263
+ if (!Array.isArray(value)) return defaultMessage(message, `${value} 类型应为数组`, optionExampleSuggestion(optionValues, "[\"选项1\", \"选项2\"]", true));
264
+ return;
265
+ }
266
+ if (typeof value === "object") return defaultMessage(message, `${value} 类型不合法`, optionExampleSuggestion(optionValues, "\"文本内容\" 或 123", false));
267
+ return;
268
+ }
269
+ const isStaticOptions = Array.isArray(config.options);
270
+ if (config.multiple) {
271
+ if (!Array.isArray(value)) return defaultMessage(message, `${value} 类型应为数组`, optionExampleSuggestion(optionValues, "[\"选项1\", \"选项2\"]", true));
272
+ if (isStaticOptions && value.some((item) => !includesOptionValue(optionValues, item))) return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(optionValues));
273
+ return;
274
+ }
275
+ if (isStaticOptions && !includesOptionValue(optionValues, value)) return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(optionValues));
276
+ };
277
+ var validateCascaderValue = (value, config, props, mForm, message) => {
278
+ const options = resolveOptions(props);
279
+ if (!options.length) return;
280
+ const valueSeparator = resolveConfig(mForm, config.valueSeparator, props);
281
+ if (isPromise(valueSeparator)) return;
282
+ const emitPath = config.emitPath !== false;
283
+ const multiple = Boolean(config.multiple);
284
+ const cascaderExample = (fallbackExample) => cascaderExampleSuggestion(options, config, valueSeparator, fallbackExample);
285
+ if (valueSeparator) {
286
+ if (typeof value !== "string" && !Array.isArray(value)) return defaultMessage(message, `${value} 类型应为字符串或数组`, cascaderExample("\"选项1,选项2\" 或 [\"选项1\", \"选项2\"]"));
287
+ } else if (multiple) {
288
+ if (!Array.isArray(value)) return defaultMessage(message, `${value} 类型应为数组`, cascaderExample("[\"选项1\", \"选项2\"]"));
289
+ } else if (emitPath && !Array.isArray(value)) return defaultMessage(message, `${value} 类型应为数组`, cascaderExample("[\"选项1\", \"选项2\"]"));
290
+ if (config.remote) return;
291
+ if (!options.length) return;
292
+ const normalizedValue = typeof value === "string" && valueSeparator ? value.split(valueSeparator) : value;
293
+ if (multiple) {
294
+ if (!Array.isArray(normalizedValue)) return defaultMessage(message, `${value} 类型应为数组`, cascaderExample("[\"选项1\", \"选项2\"]"));
295
+ if (normalizedValue.some((item) => {
296
+ if (emitPath) return !Array.isArray(item) || !isValidCascaderPath(options, item);
297
+ return !includesOptionValue(collectCascaderLeafValues(options), item);
298
+ })) return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(collectCascaderLeafValues(options)));
299
+ return;
300
+ }
301
+ if (emitPath) {
302
+ if (!Array.isArray(normalizedValue) || !isValidCascaderPath(options, normalizedValue)) return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(collectCascaderLeafValues(options)));
303
+ return;
304
+ }
305
+ if (!includesOptionValue(collectCascaderLeafValues(options), normalizedValue)) return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(collectCascaderLeafValues(options)));
306
+ };
307
+ var validateBuiltinTypeMatch = (value, fieldType, mForm, props, message) => {
308
+ if (SKIP_TYPES.has(fieldType)) return;
309
+ const config = props.config || {};
310
+ if (STRING_TYPES.has(fieldType)) {
311
+ if (config.filter === "number") {
312
+ if (typeof value !== "number" || Number.isNaN(value)) return defaultMessage(message, `${value} 类型应为数字`, typeExampleSuggestion(mForm, props, "123", isNumberValue));
313
+ return;
314
+ }
315
+ if (typeof config.filter === "function") return;
316
+ if (typeof value !== "string") return defaultMessage(message, `${value} 类型应为字符串`, typeExampleSuggestion(mForm, props, "\"文本内容\"", isStringValue));
317
+ return;
318
+ }
319
+ if (DATE_LIKE_TYPES.has(fieldType)) {
320
+ const valueFormat = resolveDateValueFormat(fieldType, config);
321
+ if (!isValidDateValueByFormat(value, valueFormat)) return dateValueFormatErrorMessage(message, valueFormat);
322
+ return;
323
+ }
324
+ if (fieldType === "number") {
325
+ if (typeof value !== "number" || Number.isNaN(value)) return defaultMessage(message, `${value} 类型应为数字`, typeExampleSuggestion(mForm, props, "123", isNumberValue));
326
+ return;
327
+ }
328
+ if (fieldType === "number-range") {
329
+ if (!Array.isArray(value) || value.length !== 2 || value.some((item) => typeof item !== "number" || Number.isNaN(item))) return defaultMessage(message, `${value} 类型应为长度为 2 的数字数组`, typeExampleSuggestion(mForm, props, "[0, 100]", isNumberRangeValue));
330
+ return;
331
+ }
332
+ if (fieldType === "switch" || fieldType === "checkbox") {
333
+ const { activeValue, inactiveValue } = resolveToggleValues(config);
334
+ if (!Object.is(value, activeValue) && !Object.is(value, inactiveValue)) return defaultMessage(message, `${value} 不在合法开关值中`, toggleSuggestion(activeValue, inactiveValue));
335
+ return;
336
+ }
337
+ if (fieldType === "select") return validateSelectValue(value, config, flattenSelectOptions(resolveOptions(props)), message);
338
+ if (fieldType === "radio-group") {
339
+ const optionValues = flattenSelectOptions(resolveOptions(props));
340
+ if (optionValues.length === 0) return;
341
+ if (!includesOptionValue(optionValues, value)) return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(optionValues));
342
+ return;
343
+ }
344
+ if (fieldType === "checkbox-group") {
345
+ const optionValues = flattenSelectOptions(resolveOptions(props));
346
+ if (optionValues.length === 0) return;
347
+ if (!Array.isArray(value)) return defaultMessage(message, `${value} 类型应为数组`, optionExampleSuggestion(optionValues, "[\"选项1\", \"选项2\"]", true));
348
+ if (value.some((item) => !includesOptionValue(optionValues, item))) return defaultMessage(message, `${value} 不在可选项中`, optionSuggestion(optionValues));
349
+ return;
350
+ }
351
+ if (fieldType === "cascader") return validateCascaderValue(value, config, props, mForm, message);
352
+ if (fieldType === "daterange" || fieldType === "timerange") {
353
+ if (config.names) return;
354
+ const valueFormat = resolveDateValueFormat(fieldType, config);
355
+ if (!Array.isArray(value) || value.length !== 2 || value.some((item) => !isValidDateValueByFormat(item, valueFormat))) return defaultMessage(message, isTimestampValueFormat(valueFormat) ? `${value} 类型应为长度为 2 的时间戳数字数组` : `${value} 格式应为长度为 2 的 ${valueFormat} 数组`, dateRangeSuggestion(valueFormat));
356
+ return;
357
+ }
358
+ if (fieldType === "table" || fieldType === "group-list" || fieldType === "grouplist") {
359
+ if (!Array.isArray(value) || value.some((item) => !isObjectValue(item))) return defaultMessage(message, `${value} 类型应为对象数组`, typeExampleSuggestion(mForm, props, "[{}]", isObjectArrayValue));
360
+ return;
361
+ }
362
+ };
363
+ var validateTypeMatch = (value, mForm, props, message) => {
364
+ if (isEmptyValue(value) || isEmptyArray(value)) return;
365
+ const rawFieldType = "type" in (props.config || {}) ? props.config.type : "";
366
+ if (typeof rawFieldType !== "string" || !rawFieldType) return;
367
+ const fieldType = toLine(rawFieldType);
368
+ const customValidator = getTypeMatchRule(fieldType);
369
+ if (customValidator) return customValidator(value, {
370
+ fieldType,
371
+ mForm,
372
+ props,
373
+ message
374
+ });
375
+ return validateBuiltinTypeMatch(value, fieldType, mForm, props, message);
376
+ };
377
+ var createTypeMatchValidator = (mForm, props, rule) => {
378
+ const originalValidator = typeof rule.validator === "function" ? rule.validator : void 0;
379
+ return (asyncValidatorRule, value, callback, source, options) => {
380
+ const actualValue = props.config?.names ? props.model : value;
381
+ try {
382
+ const error = validateTypeMatch(actualValue, mForm, props, rule.message);
383
+ if (error) {
384
+ callback(new Error(error));
385
+ return;
386
+ }
387
+ } catch (error) {
388
+ console.error(error);
389
+ }
390
+ if (originalValidator) return originalValidator({
391
+ rule: asyncValidatorRule,
392
+ value: actualValue,
393
+ callback,
394
+ source,
395
+ options
396
+ }, {
397
+ values: mForm?.initValues || {},
398
+ model: props.model,
399
+ parent: mForm?.parentValues || {},
400
+ formValue: mForm?.values || props.model,
401
+ prop: props.prop,
402
+ config: props.config
403
+ }, mForm);
404
+ callback();
405
+ };
406
+ };
407
+ //#endregion
408
+ export { clearTypeMatchRules, createTypeMatchValidator, deleteTypeMatchRule, getTypeMatchRule, registerTypeMatchRule, registerTypeMatchRules, validateTypeMatch };
package/dist/style.css CHANGED
@@ -23,7 +23,7 @@
23
23
  .m-form-dialog .m-dialog-body {
24
24
  padding: 0 20px;
25
25
  }
26
- .m-form-dialog .el-table .m-form-item .el-form-item {
26
+ .m-form-dialog .el-table .m-form-item .el-form-item:not(.is-error) {
27
27
  margin-bottom: 0;
28
28
  }
29
29
 
@@ -247,6 +247,10 @@ fieldset.m-fieldset .m-form-tip {
247
247
  width: 95vw !important;
248
248
  }
249
249
 
250
+ .el-form-item .m-fields-table .el-form-item.is-error {
251
+ margin-bottom: 18px;
252
+ }
253
+
250
254
  .m-fields-table {
251
255
  width: 100%;
252
256
  }
@@ -269,7 +273,7 @@ fieldset.m-fieldset .m-form-tip {
269
273
  .m-fields-table .el-table__expanded-cell .m-form-tip {
270
274
  margin-left: 80px;
271
275
  }
272
- .m-fields-table .el-form-item {
276
+ .m-fields-table .el-form-item:not(.is-error) {
273
277
  margin-bottom: 0;
274
278
  }
275
279
  .m-fields-table .tmagic-form-table-drag-target {