1 import {Workflow} from "../..";
2 import {PluginBase} from "../plugin-base";
4 export class SelectionPlugin extends PluginBase {
6 static edgePortsDelimiter = "$!$";
7 private svg: SVGSVGElement;
8 private selection = new Map<string, "edge" | "node">();
9 private cleanups: Function[] = [];
10 private detachModelEvents: Function | undefined;
12 private selectionChangeCallbacks: Function[] = [];
15 selected: "__selection-plugin-selected",
16 highlight: "__selection-plugin-highlight",
17 fade: "__selection-plugin-fade",
18 plugin: "__plugin-selection"
21 registerWorkflow(workflow: Workflow): void {
22 super.registerWorkflow(workflow);
24 this.svg = this.workflow.svgRoot;
26 this.svg.classList.add(this.css.plugin);
28 const clickListener = this.onClick.bind(this);
29 this.svg.addEventListener("click", clickListener);
30 this.cleanups.push(() => this.svg.removeEventListener("click", clickListener));
34 this.restoreSelection();
37 afterModelChange(): void {
38 if (typeof this.detachModelEvents === "function") {
39 this.detachModelEvents();
42 this.detachModelEvents = this.bindModelEvents();
47 if (this.detachModelEvents) {
48 this.detachModelEvents();
50 this.detachModelEvents = undefined;
52 this.svg.classList.remove(this.css.plugin);
54 for (const fn of this.cleanups) {
59 clearSelection(): void {
61 const selection: any = this.svg.querySelectorAll(`.${this.css.selected}`);
62 const highlights: any = this.svg.querySelectorAll(`.${this.css.highlight}`);
64 for (const el of selection) {
65 el.classList.remove(this.css.selected);
68 for (const el of highlights) {
69 el.classList.remove(this.css.highlight);
72 this.svg.classList.remove(this.css.fade);
74 this.selection.clear();
76 this.emitChange(null);
80 return this.selection;
83 registerOnSelectionChange(fn: (node: any) => any) {
84 this.selectionChangeCallbacks.push(fn);
87 selectStep(stepID: string) {
88 const query = `[data-connection-id="${stepID}"]`;
89 const el = this.svg.querySelector(query) as SVGElement;
92 this.materializeClickOnElement(el);
97 private bindModelEvents() {
99 const handler = () => this.restoreSelection();
100 const cleanup: any[] = [];
101 const events = ["connection.create", "connection.remove"];
103 for (const ev of events) {
104 const dispose = this.workflow.model.on(ev as any, handler);
105 cleanup.push(() => dispose.dispose());
108 return () => cleanup.forEach(fn => fn());
111 private restoreSelection() {
112 this.selection.forEach((type, connectionID) => {
114 if (type === "node") {
116 const el = this.svg.querySelector(`[data-connection-id="${connectionID}"]`) as SVGElement;
122 } else if (type === "edge") {
124 const [sID, dID] = connectionID.split(SelectionPlugin.edgePortsDelimiter);
125 const edgeSelector = `[data-source-connection="${sID}"][data-destination-connection="${dID}"]`;
127 const edge = this.svg.querySelector(edgeSelector) as SVGElement;
130 this.selectEdge(edge);
137 private onClick(click: MouseEvent): void {
138 const target = click.target as SVGElement;
140 this.clearSelection();
142 this.materializeClickOnElement(target);
145 private materializeClickOnElement(target: SVGElement) {
147 let element: SVGElement | undefined;
149 if (element = this.workflow.findParent(target, "node")) {
150 this.selectNode(element);
151 this.selection.set(element.getAttribute("data-connection-id")!, "node");
152 this.emitChange(element);
154 } else if (element = this.workflow.findParent(target, "edge")) {
155 this.selectEdge(element);
157 element.getAttribute("data-source-connection"),
158 SelectionPlugin.edgePortsDelimiter,
159 element.getAttribute("data-destination-connection")
162 this.selection.set(cid, "edge");
163 this.emitChange(cid);
167 private selectNode(element: SVGElement): void {
168 // Fade everything on canvas so we can highlight only selected stuff
169 this.svg.classList.add(this.css.fade);
171 // Mark this node as selected
172 element.classList.add(this.css.selected);
173 // Highlight it in case there are no edges on the graph
174 element.classList.add(this.css.highlight);
176 // Take all adjacent edges since we should highlight them and move them above the other edges
177 const nodeID = element.getAttribute("data-id");
178 const adjacentEdges: any = this.svg.querySelectorAll(
179 `.edge[data-source-node="${nodeID}"],` +
180 `.edge[data-destination-node="${nodeID}"]`
183 // Find the first node to be an anchor, so we can put all those edges just before that one.
184 const firstNode = this.svg.getElementsByClassName("node")[0];
186 for (const edge of adjacentEdges) {
188 // Highlight each adjacent edge
189 edge.classList.add(this.css.highlight);
191 // Move it above other edges
192 this.workflow.workflow.insertBefore(edge, firstNode);
194 // Find all adjacent nodes so we can highlight them
195 const sourceNodeID = edge.getAttribute("data-source-node");
196 const destinationNodeID = edge.getAttribute("data-destination-node");
197 const connectedNodes: any = this.svg.querySelectorAll(
198 `.node[data-id="${sourceNodeID}"],` +
199 `.node[data-id="${destinationNodeID}"]`
202 // Highlight each adjacent node
203 for (const n of connectedNodes) {
204 n.classList.add(this.css.highlight);
209 private selectEdge(element: SVGElement) {
211 element.classList.add(this.css.highlight);
212 element.classList.add(this.css.selected);
214 const sourceNode = element.getAttribute("data-source-node");
215 const destNode = element.getAttribute("data-destination-node");
216 const sourcePort = element.getAttribute("data-source-port");
217 const destPort = element.getAttribute("data-destination-port");
219 const inputPortSelector = `.node[data-id="${destNode}"] .input-port[data-port-id="${destPort}"]`;
220 const outputPortSelector = `.node[data-id="${sourceNode}"] .output-port[data-port-id="${sourcePort}"]`;
222 const connectedPorts: any = this.svg.querySelectorAll(`${inputPortSelector}, ${outputPortSelector}`);
224 for (const port of connectedPorts) {
225 port.classList.add(this.css.highlight);
229 private emitChange(change: any) {
230 for (const fn of this.selectionChangeCallbacks) {