feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,161 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { nanoid } from 'nanoid';
import {
Layer,
observeEntity,
PlaygroundConfigEntity,
SCALE_WIDTH,
} from '@flowgram-adapter/free-layout-editor';
import { domUtils } from '@flowgram-adapter/common';
interface BackgroundScaleUnit {
realSize: number;
renderSize: number;
zoom: number;
}
const PATTERN_ID = 'grid-dot-pattern';
const RENDER_SIZE = 20;
const DOT_SIZE = 1;
/**
* dot 网格背景
*/
export class BackgroundLayer extends Layer {
static type = 'WorkflowBackgroundLayer';
@observeEntity(PlaygroundConfigEntity)
protected playgroundConfigEntity: PlaygroundConfigEntity;
protected patternId = `${PATTERN_ID}${nanoid()}`;
node = domUtils.createDivWithClass('gedit-flow-background-layer');
grid: HTMLElement = document.createElement('div');
/**
* 当前缩放比
*/
get zoom(): number {
return this.config.finalScale;
}
onReady() {
const { firstChild } = this.pipelineNode;
// 背景插入到最下边
this.pipelineNode.insertBefore(this.node, firstChild);
// 初始化设置最大 200% 最小 10% 缩放
this.playgroundConfigEntity.updateConfig({
minZoom: 0.1,
maxZoom: 2,
});
// 确保点的位置在线条的下方
this.grid.style.zIndex = '-1';
this.grid.style.position = 'relative';
this.node.appendChild(this.grid);
this.grid.className = 'gedit-grid-svg';
}
/**
* 最小单元格大小
*/
getScaleUnit(): BackgroundScaleUnit {
const { zoom } = this;
return {
realSize: RENDER_SIZE, // 一个单元格代表的真实大小
renderSize: Math.round(RENDER_SIZE * zoom * 100) / 100, // 一个单元格渲染的大小值
zoom, // 缩放比
};
}
/**
* 绘制
*/
autorun(): void {
const playgroundConfig = this.playgroundConfigEntity.config;
const scaleUnit = this.getScaleUnit();
const mod = scaleUnit.renderSize * 10;
const viewBoxWidth = playgroundConfig.width + mod * 2;
const viewBoxHeight = playgroundConfig.height + mod * 2;
const { scrollX } = playgroundConfig;
const { scrollY } = playgroundConfig;
const scrollXDelta = this.getScrollDelta(scrollX, mod);
const scrollYDelta = this.getScrollDelta(scrollY, mod);
domUtils.setStyle(this.node, {
left: scrollX - SCALE_WIDTH,
top: scrollY - SCALE_WIDTH,
});
this.drawGrid(scaleUnit);
// 设置网格
this.setSVGStyle(this.grid, {
width: viewBoxWidth,
height: viewBoxHeight,
left: SCALE_WIDTH - scrollXDelta - mod,
top: SCALE_WIDTH - scrollYDelta - mod,
});
}
/**
* 绘制网格
*/
protected drawGrid(unit: BackgroundScaleUnit): void {
const minor = unit.renderSize;
if (!this.grid) {
return;
}
const patternSize = DOT_SIZE * this.zoom;
const newContent = `
<svg width="100%" height="100%">
<pattern id="${this.patternId}" width="${minor}" height="${minor}" patternUnits="userSpaceOnUse">
<circle
cx="${patternSize}"
cy="${patternSize}"
r="${patternSize}"
stroke="#eceeef"
fill-opacity="0.5"
/>
</pattern>
<rect width="100%" height="100%" fill="url(#${this.patternId})"/>
</svg>`;
this.grid.innerHTML = newContent;
}
protected setSVGStyle(
svgElement: HTMLElement | undefined,
style: { width: number; height: number; left: number; top: number },
): void {
if (!svgElement) {
return;
}
svgElement.style.width = `${style.width}px`;
svgElement.style.height = `${style.height}px`;
svgElement.style.left = `${style.left}px`;
svgElement.style.top = `${style.top}px`;
}
/**
* 获取相对滚动距离
* @param realScroll
* @param mod
*/
protected getScrollDelta(realScroll: number, mod: number): number {
// 正向滚动不用补差
if (realScroll >= 0) {
return realScroll % mod;
}
return mod - (Math.abs(realScroll) % mod);
}
}

View File

@@ -0,0 +1,309 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable complexity */
import { inject, injectable } from 'inversify';
import { SelectorBoxConfigEntity } from '@flowgram-adapter/free-layout-editor';
import { FlowNodeTransformData } from '@flowgram-adapter/free-layout-editor';
import {
EditorState,
EditorStateConfigEntity,
Layer,
PlaygroundConfigEntity,
observeEntities,
observeEntity,
observeEntityDatas,
type LayerOptions,
} from '@flowgram-adapter/free-layout-editor';
import {
WorkflowDocument,
WorkflowDragService,
WorkflowHoverService,
WorkflowLineEntity,
WorkflowLinesManager,
WorkflowNodeEntity,
WorkflowPortEntity,
WorkflowSelectService,
} from '@flowgram-adapter/free-layout-editor';
import { type IPoint } from '@flowgram-adapter/common';
import { getSelectionBounds } from '../utils/selection-utils';
import { PORT_BG_CLASS_NAME } from '../constants/points';
export interface HoverLayerOptions extends LayerOptions {
canHovered?: (e: MouseEvent, service: WorkflowHoverService) => boolean;
}
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace HoverLayerOptions {
export const DEFAULT: HoverLayerOptions = {
canHovered: () => true,
};
}
const LINE_CLASS_NAME = '.gedit-flow-activity-line';
const NODE_CLASS_NAME = '.gedit-flow-activity-node';
@injectable()
export class HoverLayer extends Layer<HoverLayerOptions> {
static type = 'HoverLayer';
@inject(WorkflowDocument) document: WorkflowDocument;
@inject(WorkflowSelectService) selectionService: WorkflowSelectService;
@inject(WorkflowDragService) dragService: WorkflowDragService;
@inject(WorkflowHoverService) hoverService: WorkflowHoverService;
@inject(WorkflowLinesManager)
linesManager: WorkflowLinesManager;
@observeEntity(EditorStateConfigEntity)
protected editorStateConfig: EditorStateConfigEntity;
@observeEntity(SelectorBoxConfigEntity)
protected selectorBoxConfigEntity: SelectorBoxConfigEntity;
@inject(PlaygroundConfigEntity) configEntity: PlaygroundConfigEntity;
/**
* 监听节点 transform
*/
@observeEntityDatas(WorkflowNodeEntity, FlowNodeTransformData)
protected readonly nodeTransforms: FlowNodeTransformData[];
/**
* 按选中排序
* @private
*/
protected nodeTransformsWithSort: FlowNodeTransformData[] = [];
autorun(): void {
const { activatedNode } = this.selectionService;
this.nodeTransformsWithSort = this.nodeTransforms
.filter(n => n.entity.id !== 'root')
.reverse() // 后创建的排在前面
.sort(n1 => (n1.entity === activatedNode ? -1 : 0));
}
/**
* 监听线条
*/
@observeEntities(WorkflowLineEntity)
protected readonly lines: WorkflowLineEntity[];
/**
* 是否正在调整线条
* @protected
*/
get isDrawing(): boolean {
return this.linesManager.isDrawing;
}
onReady(): void {
this.options = {
...HoverLayerOptions.DEFAULT,
...this.options,
};
this.toDispose.pushAll([
// 监听画布鼠标移动事件
this.listenPlaygroundEvent('mousemove', (e: MouseEvent) => {
this.hoverService.hoveredPos = this.config.getPosFromMouseEvent(e);
if (!this.isEnabled()) {
return;
}
// @ts-expect-error -- linter-disable-autofix
if (!this.options.canHovered(e, this.hoverService)) {
return;
}
const mousePos = this.config.getPosFromMouseEvent(e);
// 更新 hover 状态
this.updateHoveredState(mousePos, e?.target as HTMLElement);
}),
this.selectionService.onSelectionChanged(() => this.autorun()),
// 控制选中逻辑
this.listenPlaygroundEvent(
'mousedown',
(e: MouseEvent): boolean | undefined => {
if (!this.isEnabled() || this.isDrawing) {
return undefined;
}
const { hoveredNode } = this.hoverService;
// 重置线条
if (hoveredNode && hoveredNode instanceof WorkflowLineEntity) {
this.dragService.resetLine(hoveredNode, e);
return true;
}
if (
hoveredNode &&
hoveredNode instanceof WorkflowPortEntity &&
hoveredNode.portType !== 'input' &&
!hoveredNode.disabled &&
e.button !== 1
) {
e.stopPropagation();
e.preventDefault();
this.selectionService.clear();
this.dragService.startDrawingLine(hoveredNode, e);
return true;
}
const mousePos = this.config.getPosFromMouseEvent(e);
const selectionBounds = getSelectionBounds(
this.selectionService,
// 这里只考虑多选模式,单选模式已经下沉到 use-node-render 中
true,
);
if (
selectionBounds.width > 0 &&
selectionBounds.contains(mousePos.x, mousePos.y)
) {
/**
* 拖拽选择框
*/
this.dragService.startDragSelectedNodes(e).then(dragSuccess => {
if (!dragSuccess) {
// 拖拽没有成功触发了点击
if (hoveredNode && hoveredNode instanceof WorkflowNodeEntity) {
if (e.metaKey || e.shiftKey || e.ctrlKey) {
this.selectionService.toggleSelect(hoveredNode);
} else {
this.selectionService.selectNode(hoveredNode);
}
} else {
this.selectionService.clear();
}
}
});
// 这里会组织触发 selector box
return true;
} else {
if (!hoveredNode) {
this.selectionService.clear();
}
}
return undefined;
},
),
]);
}
/**
* 更新 hoverd
* @param mousePos
*/
updateHoveredState(mousePos: IPoint, target?: HTMLElement): void {
const nodeTransforms = this.nodeTransformsWithSort;
// // 判断连接点是否 hover
const portHovered = this.linesManager.getPortFromMousePos(mousePos);
const lineDomNodes = this.playgroundNode.querySelectorAll(LINE_CLASS_NAME);
const checkTargetFromLine = [...lineDomNodes].some(lineDom =>
lineDom.contains(target as HTMLElement),
);
// 默认 只有 output 点位可以 hover
if (portHovered) {
// 输出点可以直接选中
if (portHovered.portType === 'output') {
this.updateHoveredKey(portHovered.id);
} else if (
checkTargetFromLine ||
target?.className?.includes?.(PORT_BG_CLASS_NAME)
) {
// 输入点采用获取最接近的线条
const lineHovered =
this.linesManager.getCloseInLineFromMousePos(mousePos);
if (lineHovered) {
this.updateHoveredKey(lineHovered.id);
}
}
return;
}
// Drawing 情况,不能选中节点和线条
if (this.isDrawing) {
return;
}
const nodeHovered = nodeTransforms.find((trans: FlowNodeTransformData) =>
trans.bounds.contains(mousePos.x, mousePos.y),
)?.entity as WorkflowNodeEntity;
// 判断当前鼠标位置所在元素是否在节点内部
const nodeDomNodes = this.playgroundNode.querySelectorAll(NODE_CLASS_NAME);
const checkTargetFromNode = [...nodeDomNodes].some(nodeDom =>
nodeDom.contains(target as HTMLElement),
);
if (nodeHovered || checkTargetFromNode) {
if (nodeHovered?.id) {
this.updateHoveredKey(nodeHovered.id);
}
}
const nodeInContainer = !!(
nodeHovered?.parent && nodeHovered.parent.flowNodeType !== 'root'
);
// 获取最接近的线条
// 线条会相交需要获取最接近点位的线条,不能删除的线条不能被选中
const lineHovered = checkTargetFromLine
? this.linesManager.getCloseInLineFromMousePos(mousePos)
: undefined;
const lineInContainer = !!lineHovered?.inContainer;
// 判断容器内节点是否 hover
if (nodeHovered && nodeInContainer) {
this.updateHoveredKey(nodeHovered.id);
return;
}
// 判断容器内线条是否 hover
if (lineHovered && lineInContainer) {
this.updateHoveredKey(lineHovered.id);
return;
}
// 判断节点是否 hover
if (nodeHovered) {
this.updateHoveredKey(nodeHovered.id);
return;
}
// 判断线条是否 hover
if (lineHovered) {
this.hoverService.updateHoveredKey(lineHovered.id);
return;
}
// 上述逻辑都未命中 则清空 hoverd
this.hoverService.clearHovered();
const currentState = this.editorStateConfig.getCurrentState();
const isMouseFriendly =
currentState === EditorState.STATE_MOUSE_FRIENDLY_SELECT;
// 鼠标优先,并且不是按住 shift 键,更新为小手
if (isMouseFriendly && !this.editorStateConfig.isPressingShift) {
this.configEntity.updateCursor('grab');
}
}
updateHoveredKey(key: string): void {
// 鼠标优先交互模式,如果是 hover需要将鼠标的小手去掉还原鼠标原有样式
this.configEntity.updateCursor('default');
this.hoverService.updateHoveredKey(key);
}
/**
* 判断是否能够 hover
* @returns 是否能 hover
*/
isEnabled(): boolean {
const currentState = this.editorStateConfig.getCurrentState();
// 选择框情况禁止 hover
return (
(currentState === EditorState.STATE_SELECT ||
currentState === EditorState.STATE_MOUSE_FRIENDLY_SELECT) &&
!this.selectorBoxConfigEntity.isStart &&
!this.dragService.isDragging
);
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './lines-layer';
export * from './background-layer';
export * from './hover-layer';
export * from './shortcuts-layer';

View File

@@ -0,0 +1,233 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ReactDOM from 'react-dom';
import React from 'react';
import { inject, injectable } from 'inversify';
import {
Layer,
observeEntities,
observeEntityDatas,
TransformData,
} from '@flowgram-adapter/free-layout-editor';
import {
LineColors,
LineType,
WorkflowDocument,
WorkflowHoverService,
WorkflowLineEntity,
WorkflowLineRenderData,
WorkflowNodeEntity,
WorkflowPortEntity,
WorkflowSelectService,
} from '@flowgram-adapter/free-layout-editor';
import { domUtils } from '@flowgram-adapter/common';
import { FoldLineRender } from '../components/lines/fold-line';
import { BezierLineRender } from '../components/lines/bezier-line';
const errorActiveColor = '#FF5DC8';
@injectable()
export class LinesLayer extends Layer {
static type = 'WorkflowLinesLayer';
@inject(WorkflowHoverService) hoverService: WorkflowHoverService;
@inject(WorkflowSelectService) selectService: WorkflowSelectService;
// @observeEntity(FlowDocumentTransformerEntity)
// readonly documentTransformer: FlowDocumentTransformerEntity
@observeEntities(WorkflowLineEntity) readonly lines: WorkflowLineEntity[];
@observeEntities(WorkflowPortEntity) readonly ports: WorkflowPortEntity[];
@observeEntityDatas(WorkflowNodeEntity, TransformData)
readonly trans: TransformData[];
@inject(WorkflowDocument) protected workflowDocument: WorkflowDocument;
private _frontLineEntities: WorkflowLineEntity[] = [];
private _backLineEntities: WorkflowLineEntity[] = [];
private _version = 0;
/**
* 节点下边的线条
*/
protected backLines = domUtils.createDivWithClass(
'gedit-playground-layer gedit-flow-lines-layer back',
);
/**
* 节点前面的线条
*/
protected frontLines = domUtils.createDivWithClass(
'gedit-playground-layer gedit-flow-lines-layer front',
);
onZoom(scale: number): void {
this.backLines.style.transform = `scale(${scale})`;
this.frontLines.style.transform = `scale(${scale})`;
}
// 用来绕过 memo
private bumpVersion() {
this._version = this._version + 1;
if (this._version === Number.MAX_SAFE_INTEGER) {
this._version = 0;
}
}
onReady() {
this.pipelineNode.appendChild(this.backLines);
this.pipelineNode.appendChild(this.frontLines);
this.frontLines.style.zIndex = '20';
this.toDispose.pushAll([
this.selectService.onSelectionChanged(() => this.render()),
this.hoverService.onHoveredChange(() => this.render()),
this.workflowDocument.linesManager.onForceUpdate(() => {
this.bumpVersion();
this.render();
}),
]);
}
getLineColor(line: WorkflowLineEntity): string {
// 隐藏的优先级比 hasError 高
if (line.isHidden) {
return line.highlightColor;
}
if (line.hasError) {
if (
(this.selectService.isSelected(line.id) ||
this.hoverService.isHovered(line.id)) &&
!this.config.readonly
) {
return errorActiveColor;
}
return LineColors.ERROR;
}
if (line.highlightColor) {
return line.highlightColor;
}
if (line.drawingTo) {
return LineColors.DRAWING;
}
if (
(this.selectService.isSelected(line.id) ||
this.hoverService.isHovered(line.id)) &&
!this.config.readonly
) {
return LineColors.HOVER;
}
return LineColors.DEFUALT;
}
renderLines(lines: WorkflowLineEntity[]) {
const { lineType } = this.workflowDocument.linesManager;
// const isViewportVisible = this.config.isViewportVisible.bind(this.config);
return (
<>
{lines
.map(line => {
const color = this.getLineColor(line);
const selected = this.config.readonly
? false
: this.selectService.isSelected(line.id);
const renderData = line.getData(WorkflowLineRenderData);
const version = `${this._version}:${line.version}:${renderData.renderVersion}`;
// 正在绘制中的线条使用贝塞尔曲线
if (lineType === LineType.LINE_CHART) {
return (
<FoldLineRender
key={line.id}
color={color}
selected={selected}
line={line}
version={version}
/>
);
}
return (
<BezierLineRender
key={line.id}
color={color}
selected={selected}
line={line}
version={version}
/>
);
})
.filter(l => l)}
</>
);
}
protected isFrontLine(line: WorkflowLineEntity): boolean {
if (
this.hoverService.isHovered(line.id) ||
this.selectService.isSelected(line.id) ||
line.isDrawing
) {
return true;
}
// const { activatedNode } = this.selectService
// // 将选中的节点的连接线放置到前面
// if (activatedNode) {
// const { inputLines, outputLines } = activatedNode.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData)!
// if (inputLines.includes(line) || outputLines.includes(line)) {
// return true
// }
// }
return false;
}
renderBackLines(): React.ReactNode {
return ReactDOM.createPortal(
this.renderLines(this._backLineEntities),
this.backLines,
);
}
renderFrontLines(): React.ReactNode {
return ReactDOM.createPortal(
this.renderLines(this._frontLineEntities),
this.frontLines,
);
}
// onViewportChange = throttle(() => {
// this.render();
// }, 100);
/**
* 对线条进行分组
*/
groupLines(): void {
this._backLineEntities = [];
this._frontLineEntities = [];
this.lines.forEach(line => {
if (this.isFrontLine(line)) {
this._frontLineEntities.push(line);
} else {
this._backLineEntities.push(line);
}
});
}
render(): JSX.Element {
// const isViewportVisible = this.config.isViewportVisible.bind(this.config);
// 对线条进行分组
this.groupLines();
return (
<>
{this.renderBackLines()}
{this.renderFrontLines()}
</>
);
}
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { inject, injectable } from 'inversify';
import { Layer, SelectionService } from '@flowgram-adapter/free-layout-editor';
import {
WorkflowCommands,
WorkflowDocument,
WorkflowHoverService,
WorkflowLineEntity,
WorkflowLinesManager,
WorkflowNodeEntity,
type WorkflowNodeMeta,
} from '@flowgram-adapter/free-layout-editor';
import { WorkflowShortcutsRegistry } from '../workflow-shorcuts-contribution';
import { isShortcutsMatch } from '../utils/shortcuts-utils';
@injectable()
export class ShortcutsLayer extends Layer<object> {
static type = 'ShortcutsLayer';
@inject(WorkflowShortcutsRegistry) shortcuts: WorkflowShortcutsRegistry;
@inject(SelectionService) selection: SelectionService;
@inject(WorkflowHoverService) hoverService: WorkflowHoverService;
@inject(WorkflowDocument) document: WorkflowDocument;
@inject(WorkflowLinesManager) linesManager: WorkflowLinesManager;
onReady(): void {
this.shortcuts.addHandlersIfNotFound(
/**
* 删除
*/
{
commandId: WorkflowCommands.DELETE_NODES,
shortcuts: ['backspace', 'delete'],
isEnabled: () =>
this.selection.selection.length > 0 &&
!this.config.disabled &&
!this.config.readonly,
execute: () => {
this.selection.selection.forEach(entity => {
if (entity instanceof WorkflowNodeEntity) {
if (!this.document.canRemove(entity)) {
return;
}
const nodeMeta = entity.getNodeMeta<WorkflowNodeMeta>();
const subCanvas = nodeMeta.subCanvas?.(entity);
if (subCanvas?.isCanvas) {
subCanvas.parentNode.dispose();
return;
}
} else if (
entity instanceof WorkflowLineEntity &&
!this.linesManager.canRemove(entity)
) {
return;
}
entity.dispose();
});
this.selection.selection = this.selection.selection.filter(
s => !s.disposed,
);
},
},
/**
* 放大
*/
{
commandId: WorkflowCommands.ZOOM_IN,
shortcuts: ['meta =', 'ctrl ='],
execute: () => {
this.config.zoomin();
},
},
/**
* 缩小
*/
{
commandId: WorkflowCommands.ZOOM_OUT,
shortcuts: ['meta -', 'ctrl -'],
execute: () => {
this.config.zoomout();
},
},
);
this.toDispose.pushAll([
// 监听画布鼠标移动事件
this.listenPlaygroundEvent('keydown', (e: KeyboardEvent) => {
if (!this.isFocused || e.target !== this.playgroundNode) {
return;
}
this.shortcuts.shortcutsHandlers.some(shortcutsHandler => {
if (
isShortcutsMatch(e, shortcutsHandler.shortcuts) &&
(!shortcutsHandler.isEnabled || shortcutsHandler.isEnabled(e))
) {
shortcutsHandler.execute(e);
e.preventDefault();
return true;
}
});
}),
]);
}
}