refs #14349 Merge branch 'origin/14349-files-placeholders'
[arvados.git] / src / lib / cwl-svg / plugins / selection / selection.ts
1 import {Workflow} from "../..";
2 import {PluginBase} from "../plugin-base";
3
4 export class SelectionPlugin extends PluginBase {
5
6     static edgePortsDelimiter = "$!$";
7     private svg: SVGSVGElement;
8     private selection = new Map<string, "edge" | "node">();
9     private cleanups: Function[] = [];
10     private detachModelEvents: Function | undefined;
11
12     private selectionChangeCallbacks: Function[] = [];
13
14     private css = {
15         selected: "__selection-plugin-selected",
16         highlight: "__selection-plugin-highlight",
17         fade: "__selection-plugin-fade",
18         plugin: "__plugin-selection"
19     };
20
21     registerWorkflow(workflow: Workflow): void {
22         super.registerWorkflow(workflow);
23
24         this.svg = this.workflow.svgRoot;
25
26         this.svg.classList.add(this.css.plugin);
27
28         const clickListener = this.onClick.bind(this);
29         this.svg.addEventListener("click", clickListener);
30         this.cleanups.push(() => this.svg.removeEventListener("click", clickListener));
31     }
32
33     afterRender() {
34         this.restoreSelection();
35     }
36
37     afterModelChange(): void {
38         if (typeof this.detachModelEvents === "function") {
39             this.detachModelEvents();
40         }
41
42         this.detachModelEvents = this.bindModelEvents();
43     }
44
45     destroy() {
46
47         if (this.detachModelEvents) {
48             this.detachModelEvents();
49         }
50         this.detachModelEvents = undefined;
51
52         this.svg.classList.remove(this.css.plugin);
53
54         for (const fn of this.cleanups) {
55             fn();
56         }
57     }
58
59     clearSelection(): void {
60
61         const selection: any  = this.svg.querySelectorAll(`.${this.css.selected}`);
62         const highlights: any = this.svg.querySelectorAll(`.${this.css.highlight}`);
63
64         for (const el of selection) {
65             el.classList.remove(this.css.selected);
66         }
67
68         for (const el of highlights) {
69             el.classList.remove(this.css.highlight);
70         }
71
72         this.svg.classList.remove(this.css.fade);
73
74         this.selection.clear();
75
76         this.emitChange(null);
77     }
78
79     getSelection() {
80         return this.selection;
81     }
82
83     registerOnSelectionChange(fn: (node: any) => any) {
84         this.selectionChangeCallbacks.push(fn);
85     }
86
87     selectStep(stepID: string) {
88         const query = `[data-connection-id="${stepID}"]`;
89         const el = this.svg.querySelector(query) as SVGElement;
90
91         if (el) {
92             this.materializeClickOnElement(el);
93         }
94
95     }
96
97     private bindModelEvents() {
98
99         const handler = () => this.restoreSelection();
100         const cleanup: any[] = [];
101         const events  = ["connection.create", "connection.remove"];
102
103         for (const ev of events) {
104             const dispose = this.workflow.model.on(ev as any, handler);
105             cleanup.push(() => dispose.dispose());
106         }
107
108         return () => cleanup.forEach(fn => fn());
109     }
110
111     private restoreSelection() {
112         this.selection.forEach((type, connectionID) => {
113
114             if (type === "node") {
115
116                 const el = this.svg.querySelector(`[data-connection-id="${connectionID}"]`) as SVGElement;
117
118                 if (el) {
119                     this.selectNode(el);
120                 }
121
122             } else if (type === "edge") {
123
124                 const [sID, dID]   = connectionID.split(SelectionPlugin.edgePortsDelimiter);
125                 const edgeSelector = `[data-source-connection="${sID}"][data-destination-connection="${dID}"]`;
126
127                 const edge = this.svg.querySelector(edgeSelector) as SVGElement;
128
129                 if (edge) {
130                     this.selectEdge(edge);
131                 }
132
133             }
134         });
135     }
136
137     private onClick(click: MouseEvent): void {
138         const target = click.target as SVGElement;
139
140         this.clearSelection();
141
142         this.materializeClickOnElement(target);
143     }
144
145     private materializeClickOnElement(target: SVGElement) {
146
147         let element: SVGElement | undefined;
148
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);
153
154         } else if (element = this.workflow.findParent(target, "edge")) {
155             this.selectEdge(element);
156             const cid = [
157                 element.getAttribute("data-source-connection"),
158                 SelectionPlugin.edgePortsDelimiter,
159                 element.getAttribute("data-destination-connection")
160             ].join("");
161
162             this.selection.set(cid, "edge");
163             this.emitChange(cid);
164         }
165     }
166
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);
170
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);
175
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}"]`
181         );
182
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];
185
186         for (const edge of adjacentEdges) {
187
188             // Highlight each adjacent edge
189             edge.classList.add(this.css.highlight);
190
191             // Move it above other edges
192             this.workflow.workflow.insertBefore(edge, firstNode);
193
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}"]`
200             );
201
202             // Highlight each adjacent node
203             for (const n of connectedNodes) {
204                 n.classList.add(this.css.highlight);
205             }
206         }
207     }
208
209     private selectEdge(element: SVGElement) {
210
211         element.classList.add(this.css.highlight);
212         element.classList.add(this.css.selected);
213
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");
218
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}"]`;
221
222         const connectedPorts: any = this.svg.querySelectorAll(`${inputPortSelector}, ${outputPortSelector}`);
223
224         for (const port of connectedPorts) {
225             port.classList.add(this.css.highlight);
226         }
227     }
228
229     private emitChange(change: any) {
230         for (const fn of this.selectionChangeCallbacks) {
231             fn(change);
232         }
233     }
234 }