cloud-web-corejs 1.0.54-dev.807 → 1.0.54-dev.809

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,1055 @@
1
+ /**
2
+ * vxe-table 区域选取 / 复制粘贴(自研,替代官方付费插件 vxe-table-extend-cell-area)
3
+ *
4
+ * 挂载方式走 vxe-table 3.x 开源版预留的官方扩展点 window.VXETableMixin
5
+ * (见 vxe-table/packages/table/index.js:7),Vue.use(VXETable) 时会被 push 进
6
+ * VxeTable.mixins 并随即 delete,因此本文件必须在 Vue.use(VXETable) 之前被求值。
7
+ *
8
+ * 只实现单块矩形区域:拖拽框选 / Shift 扩选 / 方向键移动 / 复制 / 剪切 / 粘贴 /
9
+ * Del 清空 / Esc 取消。不做多区域加选、填充柄、表头整列选中、区域合并、查找替换。
10
+ *
11
+ * 与 core 的对接点:
12
+ * handleCellAreaEvent <- keyboard/mixin.js 单元格 mousedown
13
+ * handleKeyboardEvent <- methods.js 整个 keydown(含 Esc 分支)
14
+ * handleCopy/Cut/PasteCellAreaEvent <- methods.js document 上的 copy/cut/paste
15
+ * handleUpdateCellAreas <- methods.js resize/滚动/刷新后重绘
16
+ * _xxx 方法 <- methods.js 公共 API 代理
17
+ */
18
+ import XEUtils from "xe-utils";
19
+ import {
20
+ getAreaColumns,
21
+ getColumnField,
22
+ buildCellArea,
23
+ extendArea,
24
+ moveArea,
25
+ moveNextCell,
26
+ getIncrementRowSize,
27
+ encodeClipText,
28
+ encodeClipHtml,
29
+ decodeClipText,
30
+ } from "./util.js";
31
+
32
+ const AREA_CLS = "col--area";
33
+ const ACTIVE_CLS = "col--area-active";
34
+ const COPY_CLS = "col--copy-area";
35
+ const EDGE_CLS = ["col--area-t", "col--area-b", "col--area-l", "col--area-r"];
36
+ const ALL_CLS = [AREA_CLS, ACTIVE_CLS, COPY_CLS].concat(EDGE_CLS);
37
+
38
+ const ARROW_OFFSET = {
39
+ 37: [0, -1],
40
+ 38: [-1, 0],
41
+ 39: [0, 1],
42
+ 40: [1, 0],
43
+ };
44
+
45
+ // 隐藏输入框里始终放一个占位符并选中它,保证浏览器认为"有东西可复制"
46
+ const CLIP_PLACEHOLDER = " ";
47
+
48
+ function getStore($table) {
49
+ if (!$table.$_cellAreaStore) {
50
+ // 故意不放进 data:选区变化非常频繁,进了响应式会带着整表 diff
51
+ $table.$_cellAreaStore = {
52
+ area: null,
53
+ copyArea: null,
54
+ copyMatrix: null,
55
+ isCut: false,
56
+ repaintId: null,
57
+ unbindDrag: null,
58
+ };
59
+ }
60
+ return $table.$_cellAreaStore;
61
+ }
62
+
63
+ function getRowkey($table) {
64
+ return $table.rowOpts.keyField || $table.rowId || "_X_ROW_KEY";
65
+ }
66
+
67
+ function getRowid($table, row) {
68
+ let rowid = XEUtils.get(row, getRowkey($table));
69
+ return XEUtils.eqNull(rowid) ? "" : encodeURIComponent(rowid);
70
+ }
71
+
72
+ function getAreaRows($table) {
73
+ return $table.afterFullData || [];
74
+ }
75
+
76
+ function getColumns($table) {
77
+ let areaOpts = $table.areaOpts || {};
78
+ return getAreaColumns($table.visibleColumn, areaOpts.excludeFields);
79
+ }
80
+
81
+ /** 同一个单元格在主体/左固定/右固定里可能有多份 DOM,一次全取出来 */
82
+ function getCellElems($table, row, column) {
83
+ let $el = $table.$el;
84
+ if (!$el || !row || !column) return [];
85
+ let rowid = getRowid($table, row);
86
+ if (!rowid) return [];
87
+ let rowSelector = '.vxe-body--row[rowid="' + rowid + '"]';
88
+ let colSelector = '.vxe-body--column[colid="' + column.id + '"]';
89
+ return $el.querySelectorAll(rowSelector + " " + colSelector);
90
+ }
91
+
92
+ function removeAreaCls($table) {
93
+ let $el = $table.$el;
94
+ if (!$el) return;
95
+ let elems = $el.querySelectorAll("." + AREA_CLS + ", ." + COPY_CLS);
96
+ for (let i = 0; i < elems.length; i++) {
97
+ let classList = elems[i].classList;
98
+ for (let j = 0; j < ALL_CLS.length; j++) {
99
+ classList.remove(ALL_CLS[j]);
100
+ }
101
+ }
102
+ }
103
+
104
+ function paintArea($table) {
105
+ removeAreaCls($table);
106
+ let store = getStore($table);
107
+ let rows = getAreaRows($table);
108
+ let columns = getColumns($table);
109
+ let apply = function (area, isCopy) {
110
+ if (!area) return;
111
+ for (let r = area.startRow; r <= area.endRow; r++) {
112
+ let row = rows[r];
113
+ if (!row) continue;
114
+ for (let c = area.startCol; c <= area.endCol; c++) {
115
+ let column = columns[c];
116
+ if (!column) continue;
117
+ let elems = getCellElems($table, row, column);
118
+ for (let i = 0; i < elems.length; i++) {
119
+ let classList = elems[i].classList;
120
+ if (isCopy) {
121
+ classList.add(COPY_CLS);
122
+ continue;
123
+ }
124
+ classList.add(AREA_CLS);
125
+ if (r === area.startRow) classList.add(EDGE_CLS[0]);
126
+ if (r === area.endRow) classList.add(EDGE_CLS[1]);
127
+ if (c === area.startCol) classList.add(EDGE_CLS[2]);
128
+ if (c === area.endCol) classList.add(EDGE_CLS[3]);
129
+ if (r === area.anchorRow && c === area.anchorCol) {
130
+ classList.add(ACTIVE_CLS);
131
+ }
132
+ }
133
+ }
134
+ }
135
+ };
136
+ apply(store.copyArea, true);
137
+ apply(store.area, false);
138
+ }
139
+
140
+ /** 一次渲染爆发里只重绘一次 */
141
+ function scheduleRepaint($table) {
142
+ let store = getStore($table);
143
+ if (!store.area && !store.copyArea) return;
144
+ if (store.repaintId) return;
145
+ store.repaintId = window.requestAnimationFrame(function () {
146
+ store.repaintId = null;
147
+ paintArea($table);
148
+ });
149
+ }
150
+
151
+ /**
152
+ * 浏览器只在"焦点落在可编辑元素上"或"文档里有非空选区"时才派发 copy/cut/paste。
153
+ * 光有选区高亮是不够的——焦点停在 body 上时按 Ctrl+C 连事件都不会发出来,
154
+ * core 的 handleGlobalCopyEvent 自然也就跑不到。所以这里自带一个隐藏 textarea,
155
+ * 只要有选区就让它持有焦点,剪贴板事件从它身上冒泡到 document 交给 core 分发。
156
+ */
157
+ function ensureClipElem($table) {
158
+ let store = getStore($table);
159
+ if (store.clipElem && store.clipElem.parentNode) return store.clipElem;
160
+ let $el = $table.$el;
161
+ if (!$el) return null;
162
+ let elem = document.createElement("textarea");
163
+ elem.className = "vxe-table--cell-area-clip";
164
+ elem.setAttribute("tabindex", "-1");
165
+ elem.setAttribute("aria-hidden", "true");
166
+ // 不能用 display:none / visibility:hidden,那样就不可聚焦了;
167
+ // pointer-events:none 保证它不会挡住表头的鼠标事件
168
+ let hiddenStyle = "position:absolute;top:0;left:0;width:1px;height:1px;";
169
+ hiddenStyle += "padding:0;border:0;margin:0;opacity:0;resize:none;";
170
+ hiddenStyle += "overflow:hidden;pointer-events:none;";
171
+ elem.style.cssText = hiddenStyle;
172
+ elem.value = CLIP_PLACEHOLDER;
173
+ $el.appendChild(elem);
174
+ store.clipElem = elem;
175
+ return elem;
176
+ }
177
+
178
+ function focusClipElem($table) {
179
+ let elem = ensureClipElem($table);
180
+ if (!elem) return;
181
+ try {
182
+ if (elem.value !== CLIP_PLACEHOLDER) elem.value = CLIP_PLACEHOLDER;
183
+ elem.focus({ preventScroll: true });
184
+ elem.setSelectionRange(0, elem.value.length);
185
+ } catch (e) {
186
+ // 个别环境不支持 preventScroll / setSelectionRange,聚焦成功即可
187
+ }
188
+ }
189
+
190
+ function blurClipElem($table) {
191
+ let elem = getStore($table).clipElem;
192
+ if (elem && document.activeElement === elem) {
193
+ elem.blur();
194
+ }
195
+ }
196
+
197
+ function removeClipElem($table) {
198
+ let store = $table.$_cellAreaStore;
199
+ let elem = store && store.clipElem;
200
+ if (elem && elem.parentNode) {
201
+ elem.parentNode.removeChild(elem);
202
+ }
203
+ if (store) store.clipElem = null;
204
+ }
205
+
206
+ function setArea($table, area) {
207
+ getStore($table).area = area;
208
+ paintArea($table);
209
+ // 有选区就把焦点收到隐藏输入框,否则 Ctrl+C/V 根本不会触发剪贴板事件
210
+ focusClipElem($table);
211
+ }
212
+
213
+ function clearArea($table) {
214
+ getStore($table).area = null;
215
+ paintArea($table);
216
+ // 没选区了就把焦点还回去,别挡住单元格编辑器抢焦点
217
+ blurClipElem($table);
218
+ }
219
+
220
+ function toPublicArea($table, area) {
221
+ if (!area) return null;
222
+ let rows = getAreaRows($table);
223
+ let columns = getColumns($table);
224
+ return {
225
+ rows: rows.slice(area.startRow, area.endRow + 1),
226
+ cols: columns.slice(area.startCol, area.endCol + 1),
227
+ startRowIndex: area.startRow,
228
+ endRowIndex: area.endRow,
229
+ startColumnIndex: area.startCol,
230
+ endColumnIndex: area.endCol,
231
+ };
232
+ }
233
+
234
+ /** 由 row/column 反查在选区索引空间里的下标 */
235
+ function getCellIndex($table, row, column) {
236
+ let rowIndex = getAreaRows($table).indexOf(row);
237
+ let colIndex = getColumns($table).indexOf(column);
238
+ if (rowIndex < 0 || colIndex < 0) return null;
239
+ return { rowIndex: rowIndex, colIndex: colIndex };
240
+ }
241
+
242
+ /**
243
+ * 解析 setCellAreas 的一项配置为选区下标。
244
+ *
245
+ * 官方签名(见 vxe-table/types/table.d.ts 的 CellAreaConfig)是
246
+ * `{ startRow, endRow, startColumn, endColumn }`,传的是行/列对象;
247
+ * 同时兼容 `{ rows, cols }`——那是 getCellAreas() 的返回形状,
248
+ * 支持它才能「取出来的选区直接塞回去」。
249
+ */
250
+ function resolveAreaConfig($table, config) {
251
+ let rows = getAreaRows($table);
252
+ let columns = getColumns($table);
253
+ let startRowItem = config.startRow;
254
+ let endRowItem = config.endRow;
255
+ let startColItem = config.startColumn;
256
+ let endColItem = config.endColumn;
257
+ if (startRowItem === undefined && config.rows) {
258
+ let list = config.rows || [];
259
+ startRowItem = list[0];
260
+ endRowItem = list[list.length - 1];
261
+ }
262
+ if (startColItem === undefined && config.cols) {
263
+ let list = config.cols || [];
264
+ startColItem = list[0];
265
+ endColItem = list[list.length - 1];
266
+ }
267
+ let startRow = rows.indexOf(startRowItem);
268
+ let startCol = columns.indexOf(startColItem);
269
+ if (startRow < 0 || startCol < 0) return null;
270
+ // indexOf 的 -1 是"这行/列已经不在表里了"(数据刷新后拿旧引用还原选区很常见),
271
+ // 退化到起点格,绝不能当成下标 0——那会让选区朝反方向铺开
272
+ let endRow = endRowItem === undefined ? startRow : rows.indexOf(endRowItem);
273
+ let endCol = endColItem === undefined ? startCol : columns.indexOf(endColItem);
274
+ return {
275
+ startRow: startRow,
276
+ startCol: startCol,
277
+ endRow: endRow < 0 ? startRow : endRow,
278
+ endCol: endCol < 0 ? startCol : endCol,
279
+ };
280
+ }
281
+
282
+ const EDITOR_SELECTOR = [
283
+ "input:not([type=hidden]):not([disabled]):not([readonly])",
284
+ "textarea:not([disabled]):not([readonly])",
285
+ "select:not([disabled])",
286
+ '[contenteditable="true"]',
287
+ ].join(",");
288
+
289
+ // 这些控件把文本当作特殊语义(日期解析、级联、下拉过滤),塞原始字符没意义甚至有害
290
+ const NO_TYPE_THROUGH_SELECTOR = [
291
+ ".el-date-editor",
292
+ ".el-time-picker",
293
+ ".el-cascader",
294
+ ".el-select",
295
+ ".el-color-picker",
296
+ ].join(",");
297
+ // 只有这些 input 类型接受直接键入
298
+ const TEXT_INPUT_TYPES = ["", "text", "search", "tel", "url", "number"];
299
+
300
+ /**
301
+ * 把触发编辑的那个字符补进编辑器。
302
+ *
303
+ * 焦点此刻在隐藏输入框上,keydown 必须 preventDefault(否则字符会污染占位符),
304
+ * 于是用户敲的字就丢了,得再敲一次。这里在编辑器渲染出来并聚焦后把它补回去,
305
+ * 凑齐 Excel 的「选中格直接打字=覆盖着开始输入」。
306
+ */
307
+ function applyTypedChar(input, char) {
308
+ let tagName = input.tagName.toUpperCase();
309
+ if (tagName === "INPUT") {
310
+ let type = (input.getAttribute("type") || "").toLowerCase();
311
+ if (TEXT_INPUT_TYPES.indexOf(type) < 0) return;
312
+ } else if (tagName !== "TEXTAREA") {
313
+ return;
314
+ }
315
+ if (input.closest && input.closest(NO_TYPE_THROUGH_SELECTOR)) return;
316
+ input.value = char;
317
+ // 必须派发 input 事件,v-model / element-ui 才收得到这次变更
318
+ input.dispatchEvent(new Event("input", { bubbles: true }));
319
+ if (typeof input.setSelectionRange === "function") {
320
+ try {
321
+ input.setSelectionRange(char.length, char.length);
322
+ } catch (e) {
323
+ // number 类型不支持 setSelectionRange,忽略
324
+ }
325
+ }
326
+ }
327
+
328
+ /**
329
+ * 进入编辑后把焦点落到单元格里的输入控件上。
330
+ *
331
+ * core 的 handleFocus 只认 editRender.autofocus 指定的选择器,内置 input 渲染器给的是
332
+ * `'input'`(renderer.js:522)。业务列普遍写 `editRender: { name: 'input' }` 再用自定义
333
+ * edit 插槽渲染 element-ui 控件——渲染出 <textarea> 的列(比如多行文本)就选不中,
334
+ * 结果是进了编辑态却没有焦点。这里兜底找第一个可输入控件。
335
+ */
336
+ function focusCellEditor($table, row, column, typedChar) {
337
+ $table.$nextTick(function () {
338
+ let cell = $table.getCell(row, column);
339
+ if (!cell) return;
340
+ let input = cell.querySelector(EDITOR_SELECTOR);
341
+ // core 自己已经聚焦成功就别抢,但该补的字符还是要补
342
+ if (!cell.contains(document.activeElement)) {
343
+ if (!input) return;
344
+ try {
345
+ input.focus({ preventScroll: true });
346
+ } catch (e) {
347
+ input.focus();
348
+ }
349
+ }
350
+ if (typedChar) {
351
+ let target = cell.contains(document.activeElement) ? document.activeElement : input;
352
+ if (target) applyTypedChar(target, typedChar);
353
+ }
354
+ });
355
+ }
356
+
357
+ /** 让某个格进入编辑并聚焦(Backspace 与直接敲字都要用) */
358
+ function activeCellEditor($table, row, column, evnt, typedChar) {
359
+ if (!row || !column || !isColumnWritable(column)) return;
360
+ let store = getStore($table);
361
+ // 键盘触发的编辑,起手就是「非聚焦式」:方向键切单元格而不是移光标(arrowCursorLock)
362
+ store.editSessionRow = row;
363
+ store.cursorLocked = true;
364
+ $table.handleActived(buildCellParams($table, row, column), evnt);
365
+ focusCellEditor($table, row, column, typedChar);
366
+ }
367
+
368
+ function activeAnchorCell($table, area, evnt) {
369
+ activeCellEditor(
370
+ $table,
371
+ getAreaRows($table)[area.anchorRow],
372
+ getColumns($table)[area.anchorCol],
373
+ evnt
374
+ );
375
+ }
376
+
377
+ function buildCellParams($table, row, column) {
378
+ return {
379
+ $table: $table,
380
+ row: row,
381
+ rowIndex: $table.getRowIndex(row),
382
+ column: column,
383
+ columnIndex: $table.getColumnIndex(column),
384
+ cell: $table.getCell(row, column),
385
+ };
386
+ }
387
+
388
+ /** 行级可编辑判定:沿用表格自己的 beforeEditMethod,不额外引入配置 */
389
+ function isRowEditable($table, row, column) {
390
+ let editOpts = $table.editOpts || {};
391
+ let beforeEditMethod = editOpts.beforeEditMethod || editOpts.activeMethod;
392
+ if (!beforeEditMethod) return true;
393
+ return !!beforeEditMethod(buildCellParams($table, row, column));
394
+ }
395
+
396
+ function isClipExcluded($table, column) {
397
+ let clipOpts = $table.clipOpts || {};
398
+ let excludes = clipOpts.excludeFields || [];
399
+ return excludes.indexOf(getColumnField(column)) > -1;
400
+ }
401
+
402
+ /**
403
+ * 结构性可写判定:列上压根没有编辑器,说明这格用户永远改不了(单号、状态、序号、
404
+ * 创建时间这类纯展示列),Del / 剪切 / 默认粘贴就不该能改写它。
405
+ *
406
+ * 注意这只挡得住"整列只读",挡不住随行状态变化的字段级权限
407
+ * (典型如本仓库 product_problem_record 的 getColumnAuth)——
408
+ * 那种要由页面通过 keyboardConfig.delMethod / clipConfig.pasteMethod 自己把关。
409
+ */
410
+ function isColumnWritable(column) {
411
+ let editRender = column && column.editRender;
412
+ return !!editRender && editRender.enabled !== false;
413
+ }
414
+
415
+ /** 遍历选区内的每个单元格,回调拿到的是选区内的相对行列号 */
416
+ function eachAreaCell($table, area, handler) {
417
+ let rows = getAreaRows($table);
418
+ let columns = getColumns($table);
419
+ for (let r = area.startRow; r <= area.endRow; r++) {
420
+ let row = rows[r];
421
+ if (!row) continue;
422
+ for (let c = area.startCol; c <= area.endCol; c++) {
423
+ let column = columns[c];
424
+ if (!column) continue;
425
+ handler(row, column, r - area.startRow, c - area.startCol);
426
+ }
427
+ }
428
+ }
429
+
430
+ function getAreaMatrix($table, area) {
431
+ let clipOpts = $table.clipOpts || {};
432
+ let matrix = [];
433
+ eachAreaCell($table, area, function (row, column, r, c) {
434
+ if (!matrix[r]) matrix[r] = [];
435
+ if (isClipExcluded($table, column)) {
436
+ matrix[r][c] = "";
437
+ return;
438
+ }
439
+ let cellValue = $table.getCellLabel(row, column);
440
+ let text = cellValue;
441
+ if (clipOpts.copyMethod) {
442
+ text = clipOpts.copyMethod(
443
+ Object.assign(buildCellParams($table, row, column), { cellValue: cellValue })
444
+ );
445
+ }
446
+ matrix[r][c] = text === null || text === undefined ? "" : text;
447
+ });
448
+ return matrix;
449
+ }
450
+
451
+ function clearAreaValues($table, area) {
452
+ let keyboardOpts = $table.keyboardOpts || {};
453
+ eachAreaCell($table, area, function (row, column) {
454
+ if (isClipExcluded($table, column)) return;
455
+ if (!isColumnWritable(column)) return;
456
+ if (!isRowEditable($table, row, column)) return;
457
+ if (keyboardOpts.delMethod) {
458
+ keyboardOpts.delMethod(buildCellParams($table, row, column));
459
+ } else {
460
+ XEUtils.set(row, getColumnField(column), null);
461
+ }
462
+ });
463
+ $table.updateFooter();
464
+ }
465
+
466
+ /**
467
+ * 粘贴落盘。必须整段同步执行:业务的 pasteMethod 里会读 window.event.clipboardData
468
+ * 取粘贴进来的图片文件,一旦跨出原生事件派发(setTimeout/await/$nextTick)就读不到了。
469
+ */
470
+ function applyPaste($table, pasteCells, clipData) {
471
+ let store = getStore($table);
472
+ let area = store.area;
473
+ if (!area || !pasteCells.length) return;
474
+ let clipOpts = $table.clipOpts || {};
475
+ let columns = getColumns($table);
476
+
477
+ let beforeParams = {
478
+ $table: $table,
479
+ targetAreas: [toPublicArea($table, area)],
480
+ pasteCells: pasteCells,
481
+ clipData: clipData,
482
+ };
483
+ // 只有显式返回 false 才算取消;业务里常写成没有返回值的空函数
484
+ if (clipOpts.beforePasteMethod && clipOpts.beforePasteMethod(beforeParams) === false) {
485
+ return;
486
+ }
487
+
488
+ let startRowIndex = area.startRow;
489
+ let startColIndex = area.startCol;
490
+ let rows = getAreaRows($table);
491
+
492
+ if (clipOpts.isRowIncrement) {
493
+ let incrementSize = getIncrementRowSize(startRowIndex, pasteCells.length, rows.length);
494
+ if (incrementSize > 0) {
495
+ let insertRows = [];
496
+ for (let i = 0; i < incrementSize; i++) {
497
+ insertRows.push({});
498
+ }
499
+ let newRows = insertRows;
500
+ if (clipOpts.createRowsMethod) {
501
+ let created = clipOpts.createRowsMethod({
502
+ insertRows: insertRows,
503
+ pasteCells: pasteCells,
504
+ });
505
+ newRows = created || insertRows;
506
+ }
507
+ // insertAt 会把传入对象浅拷贝一份再入表,拿不到原引用;
508
+ // 但它是同步改 afterFullData 的(返回的 Promise 只是等渲染),
509
+ // 所以调用后直接重读 afterFullData 就能拿到真正入表的新行。
510
+ $table.insertAt(newRows, -1);
511
+ rows = getAreaRows($table);
512
+ }
513
+ }
514
+
515
+ let endRowIndex = startRowIndex;
516
+ let endColIndex = startColIndex;
517
+ for (let r = 0; r < pasteCells.length; r++) {
518
+ let row = rows[startRowIndex + r];
519
+ if (!row) break;
520
+ let clipRow = pasteCells[r] || [];
521
+ endRowIndex = startRowIndex + r;
522
+ for (let c = 0; c < clipRow.length; c++) {
523
+ let colIndex = startColIndex + c;
524
+ let column = columns[colIndex];
525
+ if (!column) break;
526
+ if (colIndex > endColIndex) endColIndex = colIndex;
527
+ if (isClipExcluded($table, column)) continue;
528
+ let params = Object.assign(buildCellParams($table, row, column), {
529
+ cellValue: clipRow[c],
530
+ clipData: clipData,
531
+ });
532
+ if (clipOpts.pasteMethod) {
533
+ // 有 pasteMethod 时由业务自行写值(含它自己的字段级权限判断),这里不再兜底赋值
534
+ clipOpts.pasteMethod(params);
535
+ } else if (isColumnWritable(column) && isRowEditable($table, row, column)) {
536
+ XEUtils.set(row, getColumnField(column), clipRow[c]);
537
+ }
538
+ }
539
+ }
540
+
541
+ let pastedArea = buildCellArea(startRowIndex, startColIndex);
542
+ pastedArea.focusRow = endRowIndex;
543
+ pastedArea.focusCol = endColIndex;
544
+ pastedArea.endRow = endRowIndex;
545
+ pastedArea.endCol = endColIndex;
546
+ setArea($table, pastedArea);
547
+ $table.updateFooter();
548
+
549
+ if (clipOpts.afterPasteMethod) {
550
+ clipOpts.afterPasteMethod({
551
+ $table: $table,
552
+ targetAreas: [toPublicArea($table, pastedArea)],
553
+ pasteCells: pasteCells,
554
+ clipData: clipData,
555
+ });
556
+ }
557
+ }
558
+
559
+ function stopDrag($table) {
560
+ let store = getStore($table);
561
+ if (store.unbindDrag) {
562
+ store.unbindDrag();
563
+ store.unbindDrag = null;
564
+ }
565
+ }
566
+
567
+ /** mousedown 落在输入控件上时不接管,交给控件自己处理 */
568
+ function isFormElement(target) {
569
+ if (!target || !target.tagName) return false;
570
+ let tagName = target.tagName.toUpperCase();
571
+ if (tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT") {
572
+ return true;
573
+ }
574
+ return !!(target.closest && target.closest(".vxe-cell--checkbox, .vxe-cell--radio"));
575
+ }
576
+
577
+ const cellAreaMixin = {
578
+ methods: {
579
+ /* -------------------------------- 鼠标 -------------------------------- */
580
+ handleCellAreaEvent(evnt, params) {
581
+ const $table = this;
582
+ const row = params.row;
583
+ const column = params.column;
584
+ if (evnt.button !== 0) return;
585
+ if (isFormElement(evnt.target)) {
586
+ // 鼠标点进输入框=切成「聚焦式编辑」,方向键要交还给输入框移光标
587
+ const clickStore = getStore($table);
588
+ clickStore.editSessionRow = $table.editStore.actived.row;
589
+ clickStore.cursorLocked = false;
590
+ return;
591
+ }
592
+
593
+ const actived = $table.editStore.actived;
594
+ if (actived.row) {
595
+ const editOpts = $table.editOpts || {};
596
+ const sameCell = editOpts.mode === "row" || actived.column === column;
597
+ const inActivedCell = actived.row === row && sameCell;
598
+ // 点在正在编辑的那一行/那一格里,整片都是控件,交给编辑器,不算框选
599
+ if (inActivedCell) return;
600
+ // 点到别的行/格:自己把编辑态收掉。不能指望 core 的全局 mousedown——
601
+ // 本仓库 initVxeTable 在配了 editSaveUrl 时会塞 editConfig.autoClear:false
602
+ // (components/table/index.js:837),core 那整段清理逻辑直接被跳过(methods.js:2193)
603
+ $table.clearEdit(evnt);
604
+ }
605
+
606
+ const index = getCellIndex($table, row, column);
607
+ if (!index) {
608
+ clearArea($table);
609
+ return;
610
+ }
611
+ const store = getStore($table);
612
+ store.copyArea = null;
613
+
614
+ if (evnt.shiftKey && store.area) {
615
+ setArea($table, extendArea(store.area, index.rowIndex, index.colIndex));
616
+ } else {
617
+ setArea($table, buildCellArea(index.rowIndex, index.colIndex));
618
+ }
619
+ $table.emitEvent("cell-area-selection-start", params, evnt);
620
+
621
+ // 参照 core 的 handleCheckboxRangeEvent:整段接管 document 的 move/up 再还原。
622
+ // 必须先 stopDrag 再取快照:上一次拖拽如果没收到 mouseup(鼠标拖出窗口松开、
623
+ // 中途弹窗抢焦点),它装的 handler 还挂在 document 上,先取快照会把它当成
624
+ // “原始 handler”存起来,本次拖拽结束再还原回去,之后鼠标随便动都会一直框选。
625
+ stopDrag($table);
626
+ const domMousemove = document.onmousemove;
627
+ const domMouseup = document.onmouseup;
628
+ store.unbindDrag = function () {
629
+ document.onmousemove = domMousemove;
630
+ document.onmouseup = domMouseup;
631
+ if ($table.$el) $table.$el.classList.remove("drag--range");
632
+ };
633
+ if ($table.$el) $table.$el.classList.add("drag--range");
634
+
635
+ document.onmousemove = function (moveEvnt) {
636
+ const target = moveEvnt.target;
637
+ if (!target || !target.closest) return;
638
+ const cell = target.closest(".vxe-body--column");
639
+ if (!cell || !$table.$el || !$table.$el.contains(cell)) return;
640
+ const rowNode = $table.getRowNode(cell.parentNode);
641
+ const columnNode = $table.getColumnNode(cell);
642
+ if (!rowNode || !columnNode) return;
643
+ const moveIndex = getCellIndex($table, rowNode.item, columnNode.item);
644
+ if (!moveIndex) return;
645
+ const current = getStore($table).area;
646
+ if (
647
+ current
648
+ && current.focusRow === moveIndex.rowIndex
649
+ && current.focusCol === moveIndex.colIndex
650
+ ) {
651
+ return;
652
+ }
653
+ moveEvnt.preventDefault();
654
+ setArea($table, extendArea(current, moveIndex.rowIndex, moveIndex.colIndex));
655
+ };
656
+ document.onmouseup = function (upEvnt) {
657
+ stopDrag($table);
658
+ // mousedown 的默认行为会把焦点挪到 body,晚于我们的处理,
659
+ // 所以必须在 mouseup 再收一次焦点,否则 Ctrl+C 不会触发 copy 事件
660
+ if (getStore($table).area) focusClipElem($table);
661
+ $table.emitEvent("cell-area-selection-end", params, upEvnt);
662
+ };
663
+ },
664
+
665
+ /* -------------------------------- 重绘 -------------------------------- */
666
+ handleUpdateCellAreas() {
667
+ paintArea(this);
668
+ },
669
+
670
+ /* -------------------------------- 键盘 -------------------------------- */
671
+ handleKeyboardEvent(evnt) {
672
+ const $table = this;
673
+ const store = getStore($table);
674
+ const keyboardOpts = $table.keyboardOpts || {};
675
+ const editOpts = $table.editOpts || {};
676
+ const actived = $table.editStore.actived;
677
+ const keyCode = evnt.keyCode;
678
+ const isEsc = keyCode === 27;
679
+
680
+ // 编辑态默认只处理 Esc,其余按键留给编辑器本身
681
+ // (问题描述这类列是多行 textarea,Enter 必须能正常换行)
682
+ if (actived.row || actived.column) {
683
+ if (isEsc && keyboardOpts.isEsc) {
684
+ evnt.stopPropagation();
685
+ $table.clearEdit(evnt);
686
+ return;
687
+ }
688
+ if (!keyboardOpts.arrowCursorLock) return;
689
+ // 一次新的编辑默认是「非聚焦式」:方向键切单元格,不移光标
690
+ if (store.editSessionRow !== actived.row) {
691
+ store.editSessionRow = actived.row;
692
+ store.cursorLocked = true;
693
+ }
694
+ // F2 切成「聚焦式编辑」,之后方向键归输入框
695
+ if (keyCode === 113) {
696
+ store.cursorLocked = false;
697
+ return;
698
+ }
699
+ if (!store.cursorLocked || evnt.ctrlKey || evnt.metaKey) return;
700
+ const lockOffset = ARROW_OFFSET[keyCode];
701
+ if (!lockOffset) return;
702
+ const lockRows = getAreaRows($table);
703
+ const lockCols = getColumns($table);
704
+ const fromRow = lockRows.indexOf(actived.row);
705
+ const fromCol = lockCols.indexOf(actived.column);
706
+ if (fromRow < 0 || fromCol < 0) return;
707
+ evnt.preventDefault();
708
+ // 提交并关闭当前编辑,再把选区移到目标格(不再自动进编辑,与 Excel 一致)
709
+ $table.clearEdit(evnt);
710
+ const moved = moveArea(
711
+ buildCellArea(fromRow, fromCol),
712
+ lockOffset[0],
713
+ lockOffset[1],
714
+ lockRows.length,
715
+ lockCols.length,
716
+ false
717
+ );
718
+ setArea($table, moved);
719
+ $table.scrollToRow(lockRows[moved.anchorRow], lockCols[moved.anchorCol]);
720
+ return;
721
+ }
722
+
723
+ if (isEsc) {
724
+ $table.closeFilter();
725
+ $table.closeMenu();
726
+ store.copyArea = null;
727
+ clearArea($table);
728
+ return;
729
+ }
730
+
731
+ const area = store.area;
732
+ if (!area) return;
733
+
734
+ const rows = getAreaRows($table);
735
+ const columns = getColumns($table);
736
+ const rowSize = rows.length;
737
+ const colSize = columns.length;
738
+ const hasCtrlKey = evnt.ctrlKey || evnt.metaKey;
739
+ const hasShiftKey = evnt.shiftKey;
740
+
741
+ if (!hasCtrlKey && ARROW_OFFSET[keyCode] && keyboardOpts.isArrow !== false) {
742
+ const offset = ARROW_OFFSET[keyCode];
743
+ evnt.preventDefault();
744
+ // isShift 单独控制「按住方向键以活动区域为起点向指定方向扩展」,关掉就只是移动
745
+ const extend = hasShiftKey && keyboardOpts.isShift !== false;
746
+ const next = moveArea(area, offset[0], offset[1], rowSize, colSize, extend);
747
+ setArea($table, next);
748
+ $table.scrollToRow(rows[next.focusRow], columns[next.focusCol]);
749
+ return;
750
+ }
751
+
752
+ if (keyCode === 9 && keyboardOpts.isTab !== false) {
753
+ evnt.preventDefault();
754
+ const next = moveNextCell(area, true, hasShiftKey, rowSize, colSize);
755
+ setArea($table, next);
756
+ $table.scrollToRow(rows[next.anchorRow], columns[next.anchorCol]);
757
+ return;
758
+ }
759
+
760
+ if (keyCode === 13 && keyboardOpts.isEnter !== false) {
761
+ evnt.preventDefault();
762
+ const next = moveNextCell(area, false, hasShiftKey, rowSize, colSize);
763
+ setArea($table, next);
764
+ $table.scrollToRow(rows[next.anchorRow], columns[next.anchorCol]);
765
+ return;
766
+ }
767
+
768
+ // Del 与 Backspace 是两个开关、两种语义:
769
+ // Del 只清值;Backspace 清值后还要进编辑(沿用 core 的 isBack 语义)。
770
+ // isBack 是破坏性操作,默认关闭,必须显式打开。
771
+ if (keyCode === 46) {
772
+ if (keyboardOpts.isDel) {
773
+ evnt.preventDefault();
774
+ clearAreaValues($table, area);
775
+ }
776
+ return;
777
+ }
778
+ if (keyCode === 8) {
779
+ if (keyboardOpts.isBack) {
780
+ evnt.preventDefault();
781
+ clearAreaValues($table, area);
782
+ activeAnchorCell($table, area, evnt);
783
+ }
784
+ return;
785
+ }
786
+
787
+ const isPrintable
788
+ = keyCode === 32
789
+ || (keyCode >= 48 && keyCode <= 57)
790
+ || (keyCode >= 65 && keyCode <= 90)
791
+ || (keyCode >= 96 && keyCode <= 111)
792
+ || (keyCode >= 186 && keyCode <= 192)
793
+ || (keyCode >= 219 && keyCode <= 222);
794
+ const isF2 = keyCode === 113;
795
+ if (!hasCtrlKey && (isF2 || (keyboardOpts.isEdit && isPrintable))) {
796
+ // 焦点在隐藏输入框上,必须先拦下来再判断能不能进编辑:
797
+ // 列不可编辑时若提前 return,这个字符会被真的敲进隐藏框,污染占位符
798
+ evnt.preventDefault();
799
+ const row = rows[area.anchorRow];
800
+ const column = columns[area.anchorCol];
801
+ if (!row || !column || !isColumnWritable(column)) return;
802
+ let typedChar = "";
803
+ if (!isF2) {
804
+ // 直接敲字是「覆盖」不是「追加」(Excel 语义,core 默认路径也是先清空再进编辑);
805
+ // F2 则是在原值上编辑,不清。清空复用 Del 那条通道,好继承 delMethod 里的字段级权限
806
+ clearAreaValues($table, buildCellArea(area.anchorRow, area.anchorCol));
807
+ // 清空被权限挡下了就别把字符补进去——那等于绕过 delMethod 改了这一格
808
+ if (XEUtils.eqNull(XEUtils.get(row, getColumnField(column)))) {
809
+ typedChar = evnt.key && evnt.key.length === 1 ? evnt.key : "";
810
+ }
811
+ }
812
+ if (keyboardOpts.editMethod) {
813
+ keyboardOpts.editMethod(buildCellParams($table, row, column));
814
+ } else if (editOpts.mode) {
815
+ // handleActived 内部自带 beforeEditMethod 判定,并会清掉选区
816
+ activeCellEditor($table, row, column, evnt, typedChar);
817
+ }
818
+ }
819
+ },
820
+
821
+ /* ------------------------------- 剪贴板 ------------------------------- */
822
+ /** 返回是否真的写了剪贴板,剪切要靠它决定敢不敢清源数据 */
823
+ handleCopyCellAreaEvent(evnt) {
824
+ const $table = this;
825
+ const store = getStore($table);
826
+ const clipOpts = $table.clipOpts || {};
827
+ if (clipOpts.isCopy === false) return false;
828
+ if (!store.area) return false;
829
+ const clipboardData = evnt.clipboardData || window.clipboardData;
830
+ if (!clipboardData) return false;
831
+ const matrix = getAreaMatrix($table, store.area);
832
+ evnt.preventDefault();
833
+ clipboardData.setData("text/plain", encodeClipText(matrix));
834
+ try {
835
+ // 同时写 HTML,粘进 Word/邮件/富文本才能保住表格结构;
836
+ // 个别环境只允许 text/plain,写不了就算了,不能让复制整个失败
837
+ clipboardData.setData("text/html", encodeClipHtml(matrix));
838
+ } catch (e) {
839
+ // ignore
840
+ }
841
+ store.copyMatrix = matrix;
842
+ store.copyArea = Object.assign({}, store.area);
843
+ store.isCut = false;
844
+ paintArea($table);
845
+ // 复原占位符与选中状态,保证下一次 Ctrl+C 还能触发
846
+ focusClipElem($table);
847
+ $table.emitEvent(
848
+ "cell-area-copy",
849
+ { targetAreas: [toPublicArea($table, store.area)] },
850
+ evnt
851
+ );
852
+ return true;
853
+ },
854
+
855
+ handleCutCellAreaEvent(evnt) {
856
+ const $table = this;
857
+ const store = getStore($table);
858
+ const clipOpts = $table.clipOpts || {};
859
+ if (clipOpts.isCut === false) return;
860
+ if (!store.area) return;
861
+ // 复制没成功就绝不清源,否则数据白丢
862
+ // (不能只看 store.copyArea 有没有值——那可能是上一次复制留下的)
863
+ if (!$table.handleCopyCellAreaEvent(evnt)) return;
864
+ store.isCut = true;
865
+ clearAreaValues($table, store.area);
866
+ $table.emitEvent(
867
+ "cell-area-cut",
868
+ { targetAreas: [toPublicArea($table, store.area)] },
869
+ evnt
870
+ );
871
+ },
872
+
873
+ handlePasteCellAreaEvent(evnt) {
874
+ const $table = this;
875
+ const store = getStore($table);
876
+ const clipOpts = $table.clipOpts || {};
877
+ if (clipOpts.isPaste === false) return;
878
+ if (!store.area) return;
879
+ const clipboardData = evnt.clipboardData || window.clipboardData;
880
+ if (!clipboardData) return;
881
+ const text = clipboardData.getData("text/plain") || "";
882
+ const html = clipboardData.getData("text/html") || "";
883
+ let pasteCells = decodeClipText(text);
884
+ if (!pasteCells.length) {
885
+ // 纯图片粘贴:没有文本内容,仍要把事件交给 pasteMethod 去处理附件列
886
+ if (!(clipboardData.files && clipboardData.files.length)) return;
887
+ pasteCells = [[""]];
888
+ }
889
+ evnt.preventDefault();
890
+ applyPaste($table, pasteCells, { text: text, html: html });
891
+ if (store.isCut) {
892
+ store.isCut = false;
893
+ store.copyArea = null;
894
+ paintArea($table);
895
+ }
896
+ // preventDefault 已经挡住了内容落进隐藏输入框,这里复原占位符与选中状态
897
+ focusClipElem($table);
898
+ $table.emitEvent("cell-area-paste", { pasteCells: pasteCells }, evnt);
899
+ },
900
+
901
+ /* ------------------------------ 公共 API ------------------------------ */
902
+ _getCellAreas() {
903
+ const area = getStore(this).area;
904
+ return area ? [toPublicArea(this, area)] : [];
905
+ },
906
+ _getActiveCellArea() {
907
+ const area = getStore(this).area;
908
+ if (!area) return null;
909
+ const row = getAreaRows(this)[area.anchorRow];
910
+ const column = getColumns(this)[area.anchorCol];
911
+ if (!row || !column) return null;
912
+ return buildCellParams(this, row, column);
913
+ },
914
+ _setActiveCellArea(params) {
915
+ const opts = params || {};
916
+ const index = getCellIndex(this, opts.row, opts.column);
917
+ if (index) {
918
+ setArea(this, buildCellArea(index.rowIndex, index.colIndex));
919
+ }
920
+ return this.$nextTick();
921
+ },
922
+ _setCellAreas(areas, activeArea) {
923
+ const first = (areas || [])[0];
924
+ const rect = first ? resolveAreaConfig(this, first) : null;
925
+ if (!rect) {
926
+ clearArea(this);
927
+ return this.$nextTick();
928
+ }
929
+ setArea(
930
+ this,
931
+ extendArea(buildCellArea(rect.startRow, rect.startCol), rect.endRow, rect.endCol)
932
+ );
933
+ if (activeArea) {
934
+ this._setActiveCellArea(activeArea);
935
+ }
936
+ return this.$nextTick();
937
+ },
938
+ _clearCellAreas() {
939
+ clearArea(this);
940
+ return this.$nextTick();
941
+ },
942
+ _getCopyCellArea() {
943
+ const copyArea = getStore(this).copyArea;
944
+ return copyArea ? toPublicArea(this, copyArea) : null;
945
+ },
946
+ _getCopyCellAreas() {
947
+ const copyArea = getStore(this).copyArea;
948
+ return copyArea ? [toPublicArea(this, copyArea)] : [];
949
+ },
950
+ _clearCopyCellArea() {
951
+ const store = getStore(this);
952
+ store.copyArea = null;
953
+ store.copyMatrix = null;
954
+ store.isCut = false;
955
+ paintArea(this);
956
+ return this.$nextTick();
957
+ },
958
+ // 下面三个也照样受 clipConfig 的开关约束:页面把 isCut 关掉是为了防丢数据,
959
+ // 显式调 cutCellArea() 就能绕过去的话,这个开关等于没有
960
+ // 官方签名:copyCellArea() / cutCellArea() 返回转换后的文本 { text, html },
961
+ // 不是 Promise(见 types/table.d.ts)
962
+ _copyCellArea() {
963
+ const store = getStore(this);
964
+ const clipOpts = this.clipOpts || {};
965
+ if (!store.area || clipOpts.isCopy === false) {
966
+ return { text: "", html: "" };
967
+ }
968
+ const matrix = getAreaMatrix(this, store.area);
969
+ store.copyMatrix = matrix;
970
+ store.copyArea = Object.assign({}, store.area);
971
+ store.isCut = false;
972
+ paintArea(this);
973
+ // 程序化调用也要发事件:官方 demo 就是靠 cell-area-* 同步表尾合计之类的派生数据,
974
+ // 只在键盘路径发的话,走 API 的那次刷新就漏了
975
+ this.emitEvent("cell-area-copy", { targetAreas: [toPublicArea(this, store.area)] }, null);
976
+ return { text: encodeClipText(matrix), html: encodeClipHtml(matrix) };
977
+ },
978
+ _cutCellArea() {
979
+ const store = getStore(this);
980
+ const clipOpts = this.clipOpts || {};
981
+ if (!store.area || clipOpts.isCut === false || clipOpts.isCopy === false) {
982
+ return { text: "", html: "" };
983
+ }
984
+ const result = this._copyCellArea();
985
+ store.isCut = true;
986
+ clearAreaValues(this, store.area);
987
+ this.emitEvent("cell-area-cut", { targetAreas: [toPublicArea(this, store.area)] }, null);
988
+ return result;
989
+ },
990
+ /** 无事件入口的粘贴,落的是内部记录的复制内容,不读系统剪贴板 */
991
+ _pasteCellArea() {
992
+ const store = getStore(this);
993
+ const clipOpts = this.clipOpts || {};
994
+ if (clipOpts.isPaste === false) return this.$nextTick();
995
+ if (store.copyMatrix) {
996
+ applyPaste(this, store.copyMatrix, {
997
+ text: encodeClipText(store.copyMatrix),
998
+ html: "",
999
+ });
1000
+ if (store.isCut) {
1001
+ store.isCut = false;
1002
+ store.copyArea = null;
1003
+ paintArea(this);
1004
+ }
1005
+ this.emitEvent("cell-area-paste", { pasteCells: store.copyMatrix }, null);
1006
+ }
1007
+ return this.$nextTick();
1008
+ },
1009
+ },
1010
+ created() {
1011
+ // 只管开了区域选取的表格,不去动别的表格的编辑行为
1012
+ if (!this.mouseConfig || !this.mouseOpts || !this.mouseOpts.area) return;
1013
+ // 把 core 的 handleFocus 包一层,补一次兜底聚焦。
1014
+ // 不能改成监听 edit-actived:mode=row 时 core 是在 checkValidate('blur').then() 里
1015
+ // 异步调 handleActived 的,而且同一行换列走的是「不发事件、只 setTimeout(handleFocus)」
1016
+ // 那条分支(edit/mixin.js:442)。包 handleFocus 才能覆盖双击/换列/API/键盘所有入口。
1017
+ const rawHandleFocus = this.handleFocus;
1018
+ const $table = this;
1019
+ this.handleFocus = function (params, evnt) {
1020
+ if (rawHandleFocus) rawHandleFocus.call($table, params, evnt);
1021
+ if (params) focusCellEditor($table, params.row, params.column);
1022
+ };
1023
+ },
1024
+ updated() {
1025
+ // Vue 重渲染会把打在 td 上的高亮 class 冲掉,这里补回来
1026
+ if (this.mouseConfig && this.mouseOpts && this.mouseOpts.area) {
1027
+ scheduleRepaint(this);
1028
+ }
1029
+ },
1030
+ beforeDestroy() {
1031
+ stopDrag(this);
1032
+ removeClipElem(this);
1033
+ const store = this.$_cellAreaStore;
1034
+ if (store && store.repaintId) {
1035
+ window.cancelAnimationFrame(store.repaintId);
1036
+ store.repaintId = null;
1037
+ }
1038
+ },
1039
+ };
1040
+
1041
+ if (typeof window !== "undefined") {
1042
+ // Vue.use(VXETable) 时 core 会读取并 delete 它;已有别的 mixin 时合并进去
1043
+ const exist = window.VXETableMixin;
1044
+ if (exist && exist !== cellAreaMixin) {
1045
+ window.VXETableMixin = {
1046
+ methods: Object.assign({}, exist.methods, cellAreaMixin.methods),
1047
+ updated: cellAreaMixin.updated,
1048
+ beforeDestroy: cellAreaMixin.beforeDestroy,
1049
+ };
1050
+ } else {
1051
+ window.VXETableMixin = cellAreaMixin;
1052
+ }
1053
+ }
1054
+
1055
+ export default cellAreaMixin;