vue-easytable
Version:
534 lines (481 loc) • 16.3 kB
JSX
import { clsName, getFixedTotalWidthByColumnKey } from "../util";
import { INSTANCE_METHODS } from "./constant";
import { COMPS_NAME, EMIT_EVENTS, HOOKS_NAME } from "../util/constant";
import emitter from "../../../src/mixins/emitter";
import focus from "../../../src/directives/focus.js";
import { autoResize } from "../../../src/utils/auto-resize";
import { isEmptyValue } from "../../../src/utils/index.js";
import { getCaretPosition, setCaretPosition } from "../../../src/utils/dom";
import { debounce } from "lodash";
export default {
name: COMPS_NAME.VE_TABLE_EDIT_INPUT,
directives: {
focus: focus,
},
mixins: [emitter],
props: {
parentRendered: {
type: Boolean,
required: true,
},
hooks: {
type: Object,
required: true,
},
// start input value every time
inputStartValue: {
type: [String, Number],
required: true,
},
rowKeyFieldName: {
type: String,
default: null,
},
// table data
tableData: {
type: Array,
required: true,
},
colgroups: {
type: Array,
required: true,
},
// cell selection option
cellSelectionData: {
type: Object,
required: true,
},
// editing cell
editingCell: {
type: Object,
required: true,
},
// is editing cell
isCellEditing: {
type: Boolean,
required: true,
},
// has horizontal scroll bar
hasXScrollBar: {
type: Boolean,
required: true,
},
// has vertical scroll bar
hasYScrollBar: {
type: Boolean,
required: true,
},
// has right fixed column
hasRightFixedColumn: {
type: Boolean,
required: true,
},
scrollBarWidth: {
type: Number,
required: true,
},
},
data() {
return {
textareaInputRef: "textareaInputRef",
// raw cell value
rawCellValue: "",
// display textarea
displayTextarea: false,
// virtual scroll overflowViewport
overflowViewport: false,
// textarea element rect
textareaRect: {
left: 0,
top: 0,
},
// table element
tableEl: null,
// cell element
cellEl: null,
// auto resize
autoResize: null,
// is edit cell focus
isEditCellFocus: false,
};
},
computed: {
// current column
currentColumn() {
let result = null;
const { colgroups, cellSelectionData } = this;
const { currentCell } = cellSelectionData;
if (
!isEmptyValue(currentCell.rowKey) &&
!isEmptyValue(currentCell.colKey)
) {
result = colgroups.find((x) => x.key === currentCell.colKey);
}
return result;
},
// container class
containerClass() {
let result = null;
const { displayTextarea, overflowViewport } = this;
result = {
[clsName("edit-input-container")]: true,
[clsName("edit-input-container-show")]:
displayTextarea && !overflowViewport,
};
return result;
},
// container style
containerStyle() {
let result = {};
const {
displayTextarea,
overflowViewport,
textareaRect,
currentColumn: column,
} = this;
const { top, left } = textareaRect;
if (displayTextarea && !overflowViewport) {
result = {
top: top + "px",
left: left + "px",
height: null,
// because @ve-fixed-body-cell-index: 10;
"z-index": column.fixed ? 10 : 0,
opacity: 1,
};
} else {
result = {
top: top + "px",
left: left + "px",
height: "1px",
"z-index": -1,
opacity: 0,
};
}
return result;
},
// textarea class
textareaClass() {
let result = null;
result = {
[clsName("edit-input")]: true,
};
return result;
},
},
watch: {
parentRendered: {
handler: function (val) {
if (val) {
// fixed #471
this.setTableEl();
// add table container scroll hook
this.hooks.addHook(
HOOKS_NAME.TABLE_CONTAINER_SCROLL,
() => {
if (this.displayTextarea) {
if (!this.cellEl) {
this.setCellEl();
}
}
this.debounceSetCellEl();
this.setTextareaPosition();
this.debounceSetTextareaPosition();
},
);
// add table size change hook
this.hooks.addHook(HOOKS_NAME.TABLE_SIZE_CHANGE, () => {
this.setTextareaPosition();
});
}
},
immediate: true,
},
// cell selection key data
"cellSelectionData.currentCell": {
handler: function (val) {
this.isEditCellFocus = false;
const { rowKey, colKey } = val;
if (!isEmptyValue(rowKey) && !isEmptyValue(colKey)) {
this.setCellEl();
// wait for selection cell rendered
this.$nextTick(() => {
this.setTextareaPosition();
setTimeout(() => {
this.isEditCellFocus = true;
});
});
}
},
deep: true,
immediate: true,
},
// watch normal end cell
"cellSelectionData.normalEndCell": {
handler: function (val) {
/*
trigger editor(textarea) element select
解决通过点击的区域选择,无法复制的问题
*/
if (!isEmptyValue(val.colKey)) {
this[INSTANCE_METHODS.TEXTAREA_SELECT]();
}
},
deep: true,
immediate: true,
},
// is editing cell
isCellEditing: {
handler: function (val) {
if (val) {
this.showTextarea();
} else {
this.hideTextarea();
}
},
deep: true,
immediate: true,
},
inputStartValue: {
handler: function () {
this.setRawCellValue();
},
immediate: true,
},
},
methods: {
// set table element
setTableEl() {
this.$nextTick(() => {
const tableEl = this.$el.previousElementSibling;
this.tableEl = tableEl;
});
},
// set cell element
setCellEl() {
const { cellSelectionData, tableEl } = this;
const { rowKey, colKey } = cellSelectionData.currentCell;
if (tableEl) {
const cellEl = tableEl.querySelector(
`tbody.ve-table-body tr[row-key="${rowKey}"] td[col-key="${colKey}"]`,
);
if (cellEl) {
this.cellEl = cellEl;
this.overflowViewport = false;
}
}
},
// set textarea position
setTextareaPosition() {
const {
hasXScrollBar,
hasYScrollBar,
scrollBarWidth,
colgroups,
hasRightFixedColumn,
currentColumn: column,
cellEl,
tableEl,
} = this;
if (cellEl && tableEl) {
const {
left: tableLeft,
top: tableTop,
right: tableRight,
bottom: tableBottom,
} = tableEl.getBoundingClientRect();
const {
left: cellLeft,
top: cellTop,
height: cellHeight,
width: cellWidth,
right: cellRight,
bottom: cellBottom,
} = cellEl.getBoundingClientRect();
if (cellHeight && cellWidth) {
let maxHeight = cellHeight + tableBottom - cellBottom;
let maxWidth = cellWidth + tableRight - cellRight;
// has horizontal scroll bar
if (hasXScrollBar) {
maxHeight -= scrollBarWidth;
}
// has vertical scroll bar
if (hasYScrollBar) {
maxWidth -= scrollBarWidth;
}
/*
If the right fixed column is included, the max width of the textarea needs to be subtracted from the sum of the right fixed columns
如果包含右固定列,编辑框最大宽度需要去减去右固定列之和的宽度
*/
if (hasRightFixedColumn) {
if (column && !column.fixed) {
const rightFixedTotalWidth =
getFixedTotalWidthByColumnKey({
colgroups,
colKey: column.key,
fixed: "right",
});
if (rightFixedTotalWidth) {
maxWidth -= rightFixedTotalWidth;
}
}
}
this.autoResize.init(
this.$refs[this.textareaInputRef],
{
minHeight: Math.min(cellHeight, maxHeight),
maxHeight: maxHeight, // TEXTAREA should never be higher than visible part of the viewport (should not cover the scrollbar)
minWidth: Math.min(cellWidth, maxWidth),
maxWidth: maxWidth, // TEXTAREA should never be wider than visible part of the viewport (should not cover the scrollbar)
},
true, // observe textarea change\cut\paste etc.
);
this.textareaRect = {
left: cellLeft - tableLeft,
top: cellTop - tableTop,
};
} else {
/*
存在以下可能:
1、虚拟滚动超出viewport
2、单元格被删除(通过右键菜单等方式)
*/
// fixed #477
this.textareaRect = {
left: 0,
top: 0,
};
this.cellEl = null;
this.overflowViewport = true;
}
}
},
// show textarea
showTextarea() {
this.setRawCellValue();
this.displayTextarea = true;
},
// hide textarea
hideTextarea() {
this.displayTextarea = false;
this.textareaUnObserve();
},
// textarea unObserve
textareaUnObserve() {
if (this.autoResize) {
this.autoResize.unObserve();
}
},
// set raw cell value
setRawCellValue() {
this.rawCellValue = this.inputStartValue;
},
// textarea value change
textareaValueChange(val) {
this.$emit(EMIT_EVENTS.EDIT_INPUT_VALUE_CHANGE, val);
},
// textarea select
[INSTANCE_METHODS.TEXTAREA_SELECT]() {
const textareaInputEl = this.$refs[this.textareaInputRef];
if (textareaInputEl) {
textareaInputEl.select();
}
},
// textarea add new line
[INSTANCE_METHODS.TEXTAREA_ADD_NEW_LINE]() {
const { isCellEditing, editingCell } = this;
if (isCellEditing) {
const textareaInputEl = this.$refs[this.textareaInputRef];
const caretPosition = getCaretPosition(textareaInputEl);
let value = editingCell.row[editingCell.colKey];
// solve error of number slice method
value += "";
const newValue = `${value.slice(
0,
caretPosition,
)}\n${value.slice(caretPosition)}`;
// 直接更新 textarea 值
textareaInputEl.value = newValue;
// 手动赋值不会触发textarea 文本变化事件,需要手动更新 editingCell 值
this.textareaValueChange(newValue);
setCaretPosition(textareaInputEl, caretPosition + 1);
}
},
},
created() {
// debounce set textarea position
this.debounceSetTextareaPosition = debounce(
this.setTextareaPosition,
210,
);
// debounce set cell el
this.debounceSetCellEl = debounce(() => {
if (this.displayTextarea) {
if (!this.cellEl) {
this.setCellEl();
}
}
}, 200);
},
mounted() {
this.autoResize = autoResize();
},
destroyed() {
this.textareaUnObserve();
},
render() {
const {
containerClass,
containerStyle,
textareaClass,
rawCellValue,
isCellEditing,
isEditCellFocus,
} = this;
const containerProps = {
style: containerStyle,
class: containerClass,
};
const textareaProps = {
ref: this.textareaInputRef,
class: textareaClass,
directives: [
{
name: "focus",
value: {
focus: isEditCellFocus,
},
},
],
domProps: { value: rawCellValue },
attrs: {
tabindex: -1,
},
on: {
input: (e) => {
if (isCellEditing) {
this.textareaValueChange(e.target.value);
this.rawCellValue = e.target.value;
}
},
click: () => {
this.$emit(EMIT_EVENTS.EDIT_INPUT_CLICK);
},
copy: (e) => {
this.$emit(EMIT_EVENTS.EDIT_INPUT_COPY, e);
},
paste: (e) => {
this.$emit(EMIT_EVENTS.EDIT_INPUT_PASTE, e);
},
cut: (e) => {
this.$emit(EMIT_EVENTS.EDIT_INPUT_CUT, e);
},
},
};
return (
<div {...containerProps}>
<textarea {...textareaProps}></textarea>
</div>
);
},
};