@antv/s2
Version:
effective spreadsheet render core lib
548 lines • 25.3 kB
JavaScript
import { Canvas, DisplayObject, } from '@antv/g';
import { each, get, hasIn, isEmpty, isFunction, isNil } from 'lodash';
import { GuiIcon, InteractionName } from '../common';
import { CellType, InteractionKeyboardKey, InterceptType, OriginEventType, S2Event, SHAPE_STYLE_MAP, } from '../common/constant';
import { CustomImage } from '../engine';
import { getSelectedData } from '../utils/export/copy';
import { keyEqualTo } from '../utils/export/method';
import { getAppendInfo } from '../utils/interaction/common';
import { isMobile } from '../utils/is-mobile';
import { verifyTheElementInTooltip } from '../utils/tooltip';
export class EventController {
constructor(spreadsheet) {
this.canvasEventHandlers = [];
this.s2EventHandlers = [];
this.domEventListeners = [];
this.isCanvasEffect = false;
// 不能单独判断是否 Image Shape, 用户如果自定义单元格绘制图片, 会导致判断错误
this.isGuiIconShape = (target) => {
return target instanceof CustomImage && target.imgType === GuiIcon.type;
};
this.isConditionIconShape = (target, cell) => {
return cell
.getConditionIconShapes()
.some((shape) => { var _a; return (shape === null || shape === void 0 ? void 0 : shape.name) === ((_a = target === null || target === void 0 ? void 0 : target.attributes) === null || _a === void 0 ? void 0 : _a.name); });
};
this.onCanvasMousedown = (event) => {
this.target = event.target;
// 点击时清除 hover focus 状态
this.spreadsheet.interaction.clearHoverTimer();
if (this.isResizeArea(event)) {
this.spreadsheet.emit(S2Event.LAYOUT_RESIZE_MOUSE_DOWN, event);
// 仅捕获在 Canvas 之外触发的事件 https://github.com/antvis/S2/issues/1592
const resizeMouseMoveCapture = (pointerEvent) => {
if (!this.spreadsheet.getCanvasElement()) {
return false;
}
if (this.spreadsheet.getCanvasElement() !== pointerEvent.target) {
this.spreadsheet.emit(S2Event.LAYOUT_RESIZE_MOUSE_MOVE, pointerEvent);
}
};
window.addEventListener(OriginEventType.POINTER_MOVE, resizeMouseMoveCapture);
window.addEventListener(OriginEventType.POINTER_UP, () => {
window.removeEventListener(OriginEventType.POINTER_MOVE, resizeMouseMoveCapture);
}, { once: true });
return;
}
const cellType = this.spreadsheet.getCellType(event.target);
switch (cellType) {
case CellType.DATA_CELL:
this.spreadsheet.emit(S2Event.DATA_CELL_MOUSE_DOWN, event);
break;
case CellType.ROW_CELL:
this.spreadsheet.emit(S2Event.ROW_CELL_MOUSE_DOWN, event);
break;
case CellType.COL_CELL:
this.spreadsheet.emit(S2Event.COL_CELL_MOUSE_DOWN, event);
break;
case CellType.CORNER_CELL:
this.spreadsheet.emit(S2Event.CORNER_CELL_MOUSE_DOWN, event);
break;
case CellType.MERGED_CELL:
this.spreadsheet.emit(S2Event.MERGED_CELLS_MOUSE_DOWN, event);
break;
default:
break;
}
};
this.onCanvasMousemove = (event) => {
this.canvasMousemoveEvent = event;
if (this.isResizeArea(event)) {
this.activeResizeArea(event);
this.spreadsheet.emit(S2Event.LAYOUT_RESIZE_MOUSE_MOVE, event.nativeEvent);
return;
}
this.resetResizeArea();
const cell = this.spreadsheet.getCell(event.target);
if (cell) {
const cellType = cell.cellType;
switch (cellType) {
case CellType.DATA_CELL:
this.spreadsheet.emit(S2Event.DATA_CELL_MOUSE_MOVE, event);
break;
case CellType.ROW_CELL:
this.spreadsheet.emit(S2Event.ROW_CELL_MOUSE_MOVE, event);
break;
case CellType.COL_CELL:
this.spreadsheet.emit(S2Event.COL_CELL_MOUSE_MOVE, event);
break;
case CellType.CORNER_CELL:
this.spreadsheet.emit(S2Event.CORNER_CELL_MOUSE_MOVE, event);
break;
case CellType.MERGED_CELL:
this.spreadsheet.emit(S2Event.MERGED_CELLS_MOUSE_MOVE, event);
break;
default:
break;
}
if (!this.hasBrushSelectionIntercepts()) {
this.spreadsheet.emit(S2Event.GLOBAL_HOVER, event);
switch (cellType) {
case CellType.DATA_CELL:
this.spreadsheet.emit(S2Event.DATA_CELL_HOVER, event);
break;
case CellType.ROW_CELL:
this.spreadsheet.emit(S2Event.ROW_CELL_HOVER, event);
break;
case CellType.COL_CELL:
this.spreadsheet.emit(S2Event.COL_CELL_HOVER, event);
break;
case CellType.CORNER_CELL:
this.spreadsheet.emit(S2Event.CORNER_CELL_HOVER, event);
break;
case CellType.MERGED_CELL:
this.spreadsheet.emit(S2Event.MERGED_CELLS_HOVER, event);
break;
default:
break;
}
}
}
};
this.onCanvasMouseup = (event) => {
if (this.isResizeArea(event)) {
this.spreadsheet.emit(S2Event.LAYOUT_RESIZE_MOUSE_UP, event);
return;
}
const cell = this.spreadsheet.getCell(event.target);
if (cell) {
const cellType = cell.cellType;
// 通用的 mouseup 事件
switch (cellType) {
case CellType.DATA_CELL:
this.spreadsheet.emit(S2Event.DATA_CELL_MOUSE_UP, event);
break;
case CellType.ROW_CELL:
this.spreadsheet.emit(S2Event.ROW_CELL_MOUSE_UP, event);
break;
case CellType.COL_CELL:
this.spreadsheet.emit(S2Event.COL_CELL_MOUSE_UP, event);
break;
case CellType.CORNER_CELL:
this.spreadsheet.emit(S2Event.CORNER_CELL_MOUSE_UP, event);
break;
case CellType.MERGED_CELL:
this.spreadsheet.emit(S2Event.MERGED_CELLS_MOUSE_UP, event);
break;
default:
break;
}
if (isMobile()) {
// Mobile场景下单击走这里
this.onCanvasSingleClick(event);
}
}
};
this.onCanvasClick = (event) => {
const spreadsheet = this.spreadsheet;
if (event.detail === 1 && !isMobile()) {
// PC场景下单击走这里
this.onCanvasSingleClick(event);
return;
}
if (event.detail !== 2) {
return;
}
if (this.isResizeArea(event)) {
spreadsheet.emit(S2Event.LAYOUT_RESIZE_MOUSE_UP, event);
return;
}
spreadsheet.emit(S2Event.GLOBAL_DOUBLE_CLICK, event);
const cell = spreadsheet.getCell(event.target);
if (cell) {
const cellType = cell.cellType;
if (this.target === event.target) {
switch (cellType) {
case CellType.DATA_CELL:
spreadsheet.emit(S2Event.DATA_CELL_DOUBLE_CLICK, event);
break;
case CellType.ROW_CELL:
spreadsheet.emit(S2Event.ROW_CELL_DOUBLE_CLICK, event);
break;
case CellType.COL_CELL:
spreadsheet.emit(S2Event.COL_CELL_DOUBLE_CLICK, event);
break;
case CellType.CORNER_CELL:
spreadsheet.emit(S2Event.CORNER_CELL_DOUBLE_CLICK, event);
break;
case CellType.MERGED_CELL:
spreadsheet.emit(S2Event.MERGED_CELLS_DOUBLE_CLICK, event);
break;
default:
break;
}
}
}
};
this.onCanvasMouseout = (event) => {
if (!this.isAutoResetSheetStyle(event) ||
this.isMouseOnTheCanvasContainer(event)) {
return;
}
const { interaction } = this.spreadsheet;
// 两种情况不能重置 1. 选中单元格 2. 有 intercepts 时(重置会清空 intercepts)
if (!interaction.isSelectedState() && !(interaction.intercepts.size > 0)) {
interaction.reset();
}
};
this.onCanvasContextMenu = (event) => {
const spreadsheet = this.spreadsheet;
if (this.isResizeArea(event)) {
spreadsheet.emit(S2Event.LAYOUT_RESIZE_MOUSE_UP, event);
return;
}
spreadsheet.emit(S2Event.GLOBAL_CONTEXT_MENU, event);
const cellType = this.spreadsheet.getCellType(event.target);
switch (cellType) {
case CellType.DATA_CELL:
this.spreadsheet.emit(S2Event.DATA_CELL_CONTEXT_MENU, event);
break;
case CellType.ROW_CELL:
this.spreadsheet.emit(S2Event.ROW_CELL_CONTEXT_MENU, event);
break;
case CellType.COL_CELL:
this.spreadsheet.emit(S2Event.COL_CELL_CONTEXT_MENU, event);
break;
case CellType.CORNER_CELL:
this.spreadsheet.emit(S2Event.CORNER_CELL_CONTEXT_MENU, event);
break;
case CellType.MERGED_CELL:
this.spreadsheet.emit(S2Event.MERGED_CELLS_CONTEXT_MENU, event);
break;
default:
break;
}
};
this.spreadsheet = spreadsheet;
this.bindEvents();
}
get canvasContainer() {
return this.spreadsheet.container;
}
isAutoResetSheetStyle(event) {
var _a;
const { interaction } = this.spreadsheet.options;
return isFunction(interaction === null || interaction === void 0 ? void 0 : interaction.autoResetSheetStyle)
? (_a = interaction === null || interaction === void 0 ? void 0 : interaction.autoResetSheetStyle) === null || _a === void 0 ? void 0 : _a.call(interaction, event, this.spreadsheet)
: interaction === null || interaction === void 0 ? void 0 : interaction.autoResetSheetStyle;
}
bindEvents() {
this.clearAllEvents();
// canvas events
this.addCanvasEvent(OriginEventType.MOUSE_DOWN, this.onCanvasMousedown);
this.addCanvasEvent(OriginEventType.TOUCH_START, (event) => {
this.target = event.target;
});
const realClickEvent = isMobile()
? OriginEventType.TOUCH_END
: OriginEventType.POINTER_UP;
this.addCanvasEvent(OriginEventType.POINTER_MOVE, this.onCanvasMousemove);
this.addCanvasEvent(OriginEventType.MOUSE_OUT, this.onCanvasMouseout);
this.addCanvasEvent(realClickEvent, this.onCanvasMouseup);
this.addCanvasEvent(OriginEventType.CLICK, this.onCanvasClick);
/**
* 如果监听 G Canvas, 右键对应的是 rightup/rightdown 事件, 如需禁用右键菜单 (preventDefault), 需要监听 DOM
* https://g.antv.antgroup.com/api/event/faq#%E7%A6%81%E7%94%A8%E5%8F%B3%E9%94%AE%E8%8F%9C%E5%8D%95
*/
this.addCanvasEvent(OriginEventType.RIGHT_DOWN, this.onCanvasContextMenu);
// spreadsheet events
this.addS2Event(S2Event.GLOBAL_ACTION_ICON_CLICK, () => {
this.spreadsheet.interaction.addIntercepts([InterceptType.HOVER]);
this.spreadsheet.interaction.clearState();
});
// dom events
this.addDomEventListener(window, OriginEventType.CLICK, (event) => {
this.resetSheetStyle(event);
this.isCanvasEffect = this.isMouseOnTheCanvasContainer(event);
});
this.addDomEventListener(window, OriginEventType.KEY_DOWN, (event) => {
this.onKeyboardCopy(event);
this.onKeyboardEsc(event);
this.spreadsheet.emit(S2Event.GLOBAL_KEYBOARD_DOWN, event);
});
this.addDomEventListener(window, OriginEventType.KEY_UP, (event) => {
this.spreadsheet.emit(S2Event.GLOBAL_KEYBOARD_UP, event);
});
this.addDomEventListener(window, OriginEventType.POINTER_UP, (event) => {
this.spreadsheet.emit(S2Event.GLOBAL_MOUSE_UP, event);
});
this.addDomEventListener(window, OriginEventType.POINTER_MOVE, (event) => {
this.spreadsheet.emit(S2Event.GLOBAL_MOUSE_MOVE, event);
});
}
// Windows and Mac OS
onKeyboardCopy(event) {
const { copy } = this.spreadsheet.options.interaction;
if (this.isCanvasEffect &&
(copy === null || copy === void 0 ? void 0 : copy.enable) &&
keyEqualTo(event.key, InteractionKeyboardKey.COPY) &&
(event.metaKey || event.ctrlKey)) {
const copyData = getSelectedData(this.spreadsheet);
if (!isNil(copyData)) {
this.spreadsheet.emit(S2Event.GLOBAL_COPIED, copyData);
}
}
}
onKeyboardEsc(event) {
if (this.isCanvasEffect &&
keyEqualTo(event.key, InteractionKeyboardKey.ESC)) {
this.resetSheetStyle(event);
}
}
resetSheetStyle(event) {
var _a, _b;
if (!this.isAutoResetSheetStyle(event) || !this.spreadsheet) {
return;
}
/**
* 全局有 mouseUp 和 click 事件, 当刷选完成后会同时触发, 当选中单元格后, 会同时触发 click 对应的 reset 事件
* 所以如果是 刷选过程中 引起的 click(mousedown + mouseup) 事件, 则不需要重置
*/
const { interaction } = this.spreadsheet;
if ((_a = interaction === null || interaction === void 0 ? void 0 : interaction.hasIntercepts) === null || _a === void 0 ? void 0 : _a.call(interaction, [
InterceptType.DATA_CELL_BRUSH_SELECTION,
InterceptType.COL_CELL_BRUSH_SELECTION,
InterceptType.ROW_CELL_BRUSH_SELECTION,
])) {
(_b = interaction === null || interaction === void 0 ? void 0 : interaction.removeIntercepts) === null || _b === void 0 ? void 0 : _b.call(interaction, [
InterceptType.DATA_CELL_BRUSH_SELECTION,
InterceptType.ROW_CELL_BRUSH_SELECTION,
InterceptType.COL_CELL_BRUSH_SELECTION,
]);
return;
}
if (this.isMouseOnTheTooltip(event) ||
this.isMouseOnTheCanvasContainer(event)) {
return;
}
interaction.reset();
this.spreadsheet.emit(S2Event.GLOBAL_RESET, event);
this.spreadsheet.emit(S2Event.GLOBAL_SELECTED, interaction.getActiveCells(), {
event,
targetCell: null,
interactionName: InteractionName.GLOBAL_RESET,
});
}
isMouseEvent(event) {
// 通过 MouseEvent 特有属性判断,避免 instanceof 失效的问题
return hasIn(event, 'clientX') && hasIn(event, 'clientY');
}
isMatchElement(event) {
const canvas = this.spreadsheet.getCanvasElement();
const { target } = event;
return (target === canvas ||
target instanceof DisplayObject ||
target instanceof Canvas);
}
isMatchPoint(event) {
/**
* 这里不能使用 bounding rect 的 width 和 height, 高清适配后 canvas 实际宽高会变
* 比如实际 400 * 300 => hd (800 * 600)
* 从视觉来看, 虽然点击了空白处, 但其实还是处于 放大后的 canvas 区域, 所以还需要额外判断一下坐标
*/
const canvas = this.spreadsheet.getCanvasElement();
const { width, height } = this.getContainerRect();
const { x, y } = canvas.getBoundingClientRect() || {};
const { clientX, clientY } = event;
return (clientX <= x + width &&
clientX >= x &&
clientY <= y + height &&
clientY >= y);
}
isMouseOnTheCanvasContainer(event) {
if (this.isMouseEvent(event)) {
const canvas = this.spreadsheet.getCanvasElement();
if (!canvas) {
return false;
}
// 开启 CSS transform 时, 降级处理, 不做 canvas 内的空白检测: https://github.com/antvis/S2/issues/2879
if (this.spreadsheet.getCanvasConfig().supportsCSSTransform) {
return this.isMatchElement(event);
}
return this.isMatchElement(event) && this.isMatchPoint(event);
}
return false;
}
getContainerRect() {
var _a;
const { facet, options } = this.spreadsheet;
const scrollBar = (facet === null || facet === void 0 ? void 0 : facet.hRowScrollBar) || (facet === null || facet === void 0 ? void 0 : facet.hScrollBar);
const { maxX, maxY } = (facet === null || facet === void 0 ? void 0 : facet.panelBBox) || {};
const { width = 0, height = 0 } = options;
/**
* https://github.com/antvis/S2/issues/2376
* 横向的滚动条在表格外 (Canvas 内), 点击滚动条(含滑道区域) 不应该重置交互
*/
const trackHeight = ((_a = scrollBar === null || scrollBar === void 0 ? void 0 : scrollBar.theme) === null || _a === void 0 ? void 0 : _a.size) || 0;
return {
width: Math.min(width, maxX),
height: Math.min(height, maxY + trackHeight),
};
}
isMouseOnTheTooltip(event) {
var _a, _b, _c, _d;
const { tooltip } = this.spreadsheet;
if (!(tooltip === null || tooltip === void 0 ? void 0 : tooltip.visible)) {
return false;
}
const { x, y, width, height } = ((_c = (_b = (_a = this.spreadsheet.tooltip) === null || _a === void 0 ? void 0 : _a.container) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect) === null || _c === void 0 ? void 0 : _c.call(_b)) || {};
if (event.target instanceof Node) {
return verifyTheElementInTooltip((_d = this.spreadsheet.tooltip) === null || _d === void 0 ? void 0 : _d.container, event.target);
}
if (this.isMouseEvent(event)) {
return (event.clientX >= x &&
event.clientX <= x + width &&
event.clientY >= y &&
event.clientY <= y + height);
}
return false;
}
isResizeArea(event) {
const appendInfo = getAppendInfo(event.target);
return appendInfo === null || appendInfo === void 0 ? void 0 : appendInfo.isResizeArea;
}
activeResizeArea(event) {
var _a, _b, _c;
const appendInfo = get(event.target, 'attrs.appendInfo');
if (appendInfo === null || appendInfo === void 0 ? void 0 : appendInfo.isResizeMask) {
return;
}
this.resetResizeArea();
const resizeArea = event.target;
this.spreadsheet.store.set('activeResizeArea', resizeArea);
resizeArea.attr(SHAPE_STYLE_MAP.backgroundOpacity, (_c = (_b = (_a = this.spreadsheet.theme.resizeArea) === null || _a === void 0 ? void 0 : _a.interactionState) === null || _b === void 0 ? void 0 : _b.hover) === null || _c === void 0 ? void 0 : _c.backgroundOpacity);
}
resetResizeArea() {
var _a;
const resizeArea = this.spreadsheet.store.get('activeResizeArea');
if (!isEmpty(resizeArea)) {
resizeArea.attr(SHAPE_STYLE_MAP.backgroundOpacity, (_a = this.spreadsheet.theme.resizeArea) === null || _a === void 0 ? void 0 : _a.backgroundOpacity);
}
this.spreadsheet.store.set('activeResizeArea', resizeArea);
}
hasBrushSelectionIntercepts() {
return this.spreadsheet.interaction.hasIntercepts([
InterceptType.HOVER,
InterceptType.DATA_CELL_BRUSH_SELECTION,
InterceptType.ROW_CELL_BRUSH_SELECTION,
InterceptType.COL_CELL_BRUSH_SELECTION,
]);
}
onCanvasSingleClick(event) {
const cell = this.spreadsheet.getCell(event.target);
// target 相同,说明是一个 cell 内的 click 事件
if (this.target === event.target && cell) {
// 屏蔽 actionIcons 的点击,字段标记增加的 icon 除外.
if (this.isGuiIconShape(event.target) &&
!this.isConditionIconShape(event.target, cell)) {
return;
}
this.spreadsheet.emit(S2Event.GLOBAL_CLICK, event);
switch (cell.cellType) {
case CellType.DATA_CELL:
this.spreadsheet.emit(S2Event.DATA_CELL_CLICK, event);
break;
case CellType.ROW_CELL:
this.spreadsheet.emit(S2Event.ROW_CELL_CLICK, event);
break;
case CellType.COL_CELL:
this.spreadsheet.emit(S2Event.COL_CELL_CLICK, event);
break;
case CellType.CORNER_CELL:
this.spreadsheet.emit(S2Event.CORNER_CELL_CLICK, event);
break;
case CellType.MERGED_CELL:
this.spreadsheet.emit(S2Event.MERGED_CELLS_CLICK, event);
break;
default:
break;
}
}
}
clear() {
this.unbindEvents();
}
unbindEvents() {
this.clearAllEvents();
}
addCanvasEvent(eventType, handler) {
var _a;
(_a = this.canvasContainer) === null || _a === void 0 ? void 0 : _a.on(eventType, handler);
this.canvasEventHandlers.push({ type: eventType, handler });
}
addS2Event(eventType, handler) {
this.spreadsheet.on(eventType, handler);
this.s2EventHandlers.push({
type: eventType,
handler,
});
}
addDomEventListener(target, type, handler) {
if (target.addEventListener) {
const { eventListenerOptions } = this.spreadsheet.options.interaction;
target.addEventListener(type, handler, eventListenerOptions);
this.domEventListeners.push({
target,
type,
handler: handler,
options: eventListenerOptions,
});
}
else {
// eslint-disable-next-line no-console
console.error(`Please make sure ${target} has addEventListener function`);
}
}
clearAllEvents() {
each(this.canvasEventHandlers, ({ type, handler }) => {
var _a;
(_a = this.canvasContainer) === null || _a === void 0 ? void 0 : _a.removeEventListener(type, handler);
});
each(this.s2EventHandlers, ({ type, handler }) => {
this.spreadsheet.off(type, handler);
});
each(this.domEventListeners, (listener) => {
listener.target.removeEventListener(listener.type, listener.handler, listener.options);
});
this.canvasEventHandlers = [];
this.s2EventHandlers = [];
this.domEventListeners = [];
}
getViewportPoint(event) {
const config = this.spreadsheet.getCanvasConfig();
// https://github.com/antvis/G/blob/a43c19a662684945d0bf9dc1876af43ac26b1243/packages/g-lite/src/plugins/EventPlugin.ts#L216
if (config.supportsCSSTransform &&
!isNil(event.offsetX) &&
!isNil(event.offsetY)) {
return {
x: event.offsetX,
y: event.offsetY,
};
}
return this.spreadsheet.container.client2Viewport({
x: event.clientX,
y: event.clientY,
});
}
}
//# sourceMappingURL=event-controller.js.map