Add flow editor toolbar and lock readonly graph drag
This commit is contained in:
parent
533a1e7f43
commit
d01344c19e
|
|
@ -12,6 +12,74 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rete-editor-toolbar {
|
||||
position: sticky;
|
||||
top: 12px;
|
||||
left: 0;
|
||||
z-index: 36;
|
||||
display: inline-flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
padding-left: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.rete-editor-toolbar-group {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding: 12px 10px 10px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.72);
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
box-shadow: 0 14px 28px rgba(15, 23, 42, 0.14);
|
||||
backdrop-filter: blur(12px);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.rete-editor-toolbar-legend {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: 12px;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.7);
|
||||
border-radius: 999px;
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.rete-editor-toolbar-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border: 1px solid rgba(203, 213, 225, 0.95);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
||||
color: #0f172a;
|
||||
font-size: 16px;
|
||||
transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.rete-editor-toolbar-btn:hover {
|
||||
border-color: #94a3b8;
|
||||
box-shadow: 0 10px 18px rgba(15, 23, 42, 0.12);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.rete-editor-toolbar-btn-active {
|
||||
border-color: #0f766e;
|
||||
background: linear-gradient(180deg, #ccfbf1 0%, #99f6e4 100%);
|
||||
color: #134e4a;
|
||||
box-shadow: inset 0 0 0 1px rgba(15, 118, 110, 0.18);
|
||||
}
|
||||
|
||||
.rete-editor-selection-badge {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,48 @@
|
|||
(pointermove)="onShellPointerMove($event)"
|
||||
(pointerup)="onShellPointerUp($event)"
|
||||
(pointercancel)="onShellPointerUp($event)">
|
||||
@if (!readonly()) {
|
||||
<div class="rete-editor-toolbar">
|
||||
<section class="rete-editor-toolbar-group" aria-label="Editor mode controls">
|
||||
<div class="rete-editor-toolbar-legend">Mode</div>
|
||||
<button
|
||||
type="button"
|
||||
class="rete-editor-toolbar-btn"
|
||||
[class.rete-editor-toolbar-btn-active]="!selectionModeActive"
|
||||
(click)="setEditorMode('standard', $event)"
|
||||
aria-label="Standard mode">
|
||||
<i class="bi bi-cursor" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rete-editor-toolbar-btn"
|
||||
[class.rete-editor-toolbar-btn-active]="selectionModeActive"
|
||||
(click)="setEditorMode('select', $event)"
|
||||
aria-label="Selection mode">
|
||||
<i class="bi bi-bounding-box" aria-hidden="true"></i>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="rete-editor-toolbar-group" aria-label="Zoom controls">
|
||||
<div class="rete-editor-toolbar-legend">Zoom</div>
|
||||
<button
|
||||
type="button"
|
||||
class="rete-editor-toolbar-btn"
|
||||
(click)="zoomIn($event)"
|
||||
aria-label="Zoom in">
|
||||
<i class="bi bi-zoom-in" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rete-editor-toolbar-btn"
|
||||
(click)="zoomOut($event)"
|
||||
aria-label="Zoom out">
|
||||
<i class="bi bi-zoom-out" aria-hidden="true"></i>
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="h-full w-full" #editor (dragover)="onDragOver($event)" (drop)="onDrop($event)">
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Component, ElementRef, Injector, input, OnChanges, OnDestroy, output, signal, SimpleChanges, ViewChild } from '@angular/core';
|
||||
import { BlockType, FlowData, FlowNode } from '@models/flow';
|
||||
import { Drag } from 'rete-area-plugin';
|
||||
import { BlocksService } from '@services/blocks/blocks';
|
||||
import { ContainersService } from '@services/containers/containers';
|
||||
import { BLOCK_TYPE_DRAG_MIME } from '@shared/blocks-list/block-drag';
|
||||
|
|
@ -36,6 +37,7 @@ export class ReteEditor implements OnChanges, OnDestroy {
|
|||
private suppressDirtyEvents = false;
|
||||
creatingEmptyBlock = false;
|
||||
creatingEmptyBlockType = '';
|
||||
editorMode = signal<'standard' | 'select'>('standard');
|
||||
selectionBox = signal<{ left: number; top: number; width: number; height: number } | null>(null);
|
||||
private selectionPointerId: number | null = null;
|
||||
private selectionStart: { x: number; y: number } | null = null;
|
||||
|
|
@ -75,6 +77,10 @@ export class ReteEditor implements OnChanges, OnDestroy {
|
|||
return !this.readonly() && this.selectedBlockCount > 0;
|
||||
}
|
||||
|
||||
get selectionModeActive() {
|
||||
return this.editorMode() === 'select';
|
||||
}
|
||||
|
||||
onDragOver(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) {
|
||||
|
|
@ -130,6 +136,31 @@ export class ReteEditor implements OnChanges, OnDestroy {
|
|||
this.shell.nativeElement.setPointerCapture(event.pointerId);
|
||||
}
|
||||
|
||||
setEditorMode(mode: 'standard' | 'select', event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
if (this.readonly()) return;
|
||||
this.editorMode.set(mode);
|
||||
this.syncAreaDragMode();
|
||||
if (mode === 'standard') {
|
||||
this.selectionPointerId = null;
|
||||
this.selectionStart = null;
|
||||
this.selectionBox.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
zoomIn(event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
void this.applyZoom(1.12);
|
||||
}
|
||||
|
||||
zoomOut(event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
void this.applyZoom(1 / 1.12);
|
||||
}
|
||||
|
||||
onShellPointerMove(event: PointerEvent) {
|
||||
if (this.selectionPointerId !== event.pointerId || !this.selectionStart) return;
|
||||
|
||||
|
|
@ -193,7 +224,8 @@ export class ReteEditor implements OnChanges, OnDestroy {
|
|||
host.innerHTML = '';
|
||||
|
||||
const rete = await createEditor(host, this.injector, this.flowData(), {
|
||||
nodeView: this.nodeView()
|
||||
nodeView: this.nodeView(),
|
||||
readonly: this.readonly()
|
||||
});
|
||||
if (currentVersion !== this.loadVersion) {
|
||||
rete.area.destroy();
|
||||
|
|
@ -201,6 +233,7 @@ export class ReteEditor implements OnChanges, OnDestroy {
|
|||
}
|
||||
|
||||
this.rete = rete;
|
||||
this.syncAreaDragMode();
|
||||
const loadedFlowId = this.flowId();
|
||||
const normalizedData = exportGraph(rete.editor);
|
||||
if (this.flowState.currentFlow()?.id === loadedFlowId) {
|
||||
|
|
@ -270,6 +303,7 @@ export class ReteEditor implements OnChanges, OnDestroy {
|
|||
}
|
||||
|
||||
private canStartSelection(target: EventTarget | null) {
|
||||
if (!this.selectionModeActive) return false;
|
||||
const element = target instanceof HTMLElement ? target : null;
|
||||
if (!element) return false;
|
||||
if (element.closest('[data-testid="node"]')) return false;
|
||||
|
|
@ -277,6 +311,13 @@ export class ReteEditor implements OnChanges, OnDestroy {
|
|||
return true;
|
||||
}
|
||||
|
||||
private syncAreaDragMode() {
|
||||
const area = this.rete?.area?.area;
|
||||
if (!area) return;
|
||||
|
||||
area.setDragHandler(this.selectionModeActive ? null : new Drag());
|
||||
}
|
||||
|
||||
private resolveBlocksInsideSelection() {
|
||||
const selection = this.selectionBox();
|
||||
if (!selection) return [];
|
||||
|
|
@ -308,4 +349,16 @@ export class ReteEditor implements OnChanges, OnDestroy {
|
|||
|| targetRect.top > selectionRect.bottom
|
||||
);
|
||||
}
|
||||
|
||||
private async applyZoom(multiplier: number) {
|
||||
const area = this.rete?.area?.area;
|
||||
const host = this.container?.nativeElement as HTMLElement | undefined;
|
||||
if (!area || !host) return;
|
||||
|
||||
const currentZoom = area.transform.k || 1;
|
||||
const nextZoom = Math.min(2.4, Math.max(0.35, currentZoom * multiplier));
|
||||
if (Math.abs(nextZoom - currentZoom) < 0.001) return;
|
||||
|
||||
await area.zoom(nextZoom, 0, 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export async function createEditor(
|
|||
container: HTMLElement,
|
||||
injector: Injector,
|
||||
flowData: FlowData,
|
||||
options?: { nodeView?: "editor" | "execution" }
|
||||
options?: { nodeView?: "editor" | "execution"; readonly?: boolean }
|
||||
): Promise<ReteEditorInstance> {
|
||||
|
||||
const editor = new NodeEditor<HFSchemes>();
|
||||
|
|
@ -55,6 +55,7 @@ export async function createEditor(
|
|||
const connection = new ConnectionPlugin<HFSchemes, AreaExtra>();
|
||||
const render = new AngularPlugin<HFSchemes, AreaExtra>({ injector });
|
||||
const nodeView = options?.nodeView ?? "editor";
|
||||
const readonly = options?.readonly === true;
|
||||
const runtime: ReteRuntimeContext = {
|
||||
blocksService: injector.get(BlocksService),
|
||||
containersService: injector.get(ContainersService),
|
||||
|
|
@ -107,6 +108,11 @@ export async function createEditor(
|
|||
|
||||
AreaExtensions.simpleNodesOrder(area);
|
||||
|
||||
area.addPipe((context: any) => {
|
||||
if (readonly && context?.type === 'nodetranslate') return;
|
||||
return context;
|
||||
});
|
||||
|
||||
editor.use(area);
|
||||
area.use(connection);
|
||||
area.use(render);
|
||||
|
|
|
|||
Loading…
Reference in New Issue