Laas Dashboard Front End Improvements
[laas.git] / src / static / js / dashboard.js
1 ///////////////////
2 // Global Variables
3 ///////////////////
4
5 form_submission_callbacks = [];  //all runnables will be executed before form submission
6
7 ///////////////////
8 // Global Functions
9 ///////////////////
10
11 // Taken from https://docs.djangoproject.com/en/3.0/ref/csrf/
12 function getCookie(name) {
13     var cookieValue = null;
14     if (document.cookie && document.cookie !== '') {
15         var cookies = document.cookie.split(';');
16         for (var i = 0; i < cookies.length; i++) {
17             var cookie = cookies[i].trim();
18             // Does this cookie string begin with the name we want?
19             if (cookie.substring(0, name.length + 1) === (name + '=')) {
20                 cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
21                 break;
22             }
23         }
24     }
25     return cookieValue;
26 }
27
28 function update_page(response) {
29     if( response.redirect )
30     {
31         window.location.replace(response.redirect);
32         return;
33     }
34     draw_breadcrumbs(response.meta);
35     update_exit_button(response.meta);
36     update_side_buttons(response.meta);
37     $("#formContainer").html(response.content);
38 }
39
40 function update_side_buttons(meta) {
41     const step = meta.active;
42     const page_count = meta.steps.length;
43
44     const back_button = document.getElementById("workflow-nav-back");
45     if (step == 0) {
46         back_button.classList.add("disabled");
47         back_button.disabled = true;
48     } else {
49         back_button.classList.remove("disabled");
50         back_button.disabled = false;
51     }
52
53     const forward_btn = document.getElementById("workflow-nav-next");
54     if (step == page_count - 1) {
55         forward_btn.classList.add("disabled");
56         forward_btn.disabled = true;
57     } else {
58         forward_btn.classList.remove("disabled");
59         forward_btn.disabled = false;
60     }
61 }
62
63 function update_exit_button(meta) {
64     if (meta.workflow_count == 1) {
65         document.getElementById("cancel_btn").innerText = "Exit Workflow";
66     } else {
67         document.getElementById("cancel_btn").innerText = "Return to Parent";
68     }
69 }
70
71 function draw_breadcrumbs(meta) {
72     $("#topPagination").children().not(".page-control").remove();
73
74     for (const i in meta.steps) {
75         const step_btn = create_step(meta.steps[i], i == meta["active"]);
76         $("#topPagination li:last-child").before(step_btn);
77     }
78 }
79
80 function create_step(step_json, active) {
81     const step_dom = document.createElement("li");
82     // First create the dom object depending on active or not
83     step_dom.className = "topcrumb";
84     if (active) {
85         step_dom.classList.add("active");
86     }
87     $(step_dom).html(`<span class="d-flex align-items-center justify-content-center text-capitalize w-100">${step_json['title']}</span>`)
88
89     const code = step_json.valid;
90
91     let stat = "";
92     let msg = "";
93     if (code < 100) {
94         $(step_dom).children().first().append("<i class='ml-2 far fa-square'></i>")
95         stat = "";
96         msg = "";
97     } else if (code < 200) {
98         $(step_dom).children().first().append("<i class='ml-2 fas fa-minus-square'></i>")
99         stat = "invalid";
100         msg = step_json.message;
101     } else if (code < 300) {
102         $(step_dom).children().first().append("<i class='ml-2 far fa-check-square'></i>")
103         stat = "valid";
104         msg = step_json.message;
105     }
106
107     if (step_json.enabled == false) {
108         step_dom.classList.add("disabled");
109     }
110     if (active) {
111         update_message(msg, stat);
112     }
113
114     return step_dom;
115 }
116
117 function update_description(title, desc) {
118     document.getElementById("view_title").innerText = title;
119     document.getElementById("view_desc").innerText = desc;
120 }
121
122 function update_message(message, stepstatus) {
123     let color_code;
124     if (stepstatus == 'valid') {
125         color_code = 'text-success';
126     } else if (stepstatus == 'invalid') {
127         color_code = 'text-danger';
128     } else {
129         color_code = 'none';
130     }
131     document.getElementById("view_message").innerText = message;
132     document.getElementById("view_message").className = "step_message";
133     document.getElementById("view_message").classList.add("message_" + stepstatus);
134     document.getElementById("view_message").classList.add(color_code);
135 }
136
137 function submitStepForm(next_step = "current"){
138     run_form_callbacks();
139     const step_form_data = $("#step_form").serialize();
140     const form_data = $.param({
141         "step": next_step,
142         "step_form": step_form_data,
143         "csrfmiddlewaretoken": $("[name=csrfmiddlewaretoken]").val()
144     });
145     $.post(
146         '/workflow/manager/',
147         form_data,
148         (data) => update_page(data),
149         'json'
150     ).fail(() => alert("failure"));
151 }
152
153 function run_form_callbacks(){
154     for(f of form_submission_callbacks)
155         f();
156     form_submission_callbacks = [];
157 }
158
159 function create_workflow(type) {
160     $.ajax({
161         type: "POST",
162         url: "/workflow/create/",
163         data: {
164             "workflow_type": type
165         },
166         headers: {
167             "X-CSRFToken": getCookie('csrftoken')
168         }
169     }).done(function (data, textStatus, jqXHR) {
170         window.location = "/workflow/";
171     }).fail(function (jqxHR, textstatus) {
172         alert("Something went wrong...");
173     });
174 }
175
176 function add_workflow(type) {
177     data = $.ajax({
178         type: "POST",
179         url: "/workflow/add/",
180         data: {
181             "workflow_type": type
182         },
183         headers: {
184             "X-CSRFToken": getCookie('csrftoken')
185         }
186     }).done(function (data, textStatus, jqXHR) {
187         update_page(data);
188     }).fail(function (jqxHR, textstatus) {
189         alert("Something went wrong...");
190     });
191 }
192
193 function pop_workflow() {
194     data = $.ajax({
195         type: "POST",
196         url: "/workflow/pop/",
197         headers: {
198             "X-CSRFToken": getCookie('csrftoken')
199         }
200     }).done(function (data, textStatus, jqXHR) {
201         update_page(data);
202     }).fail(function (jqxHR, textstatus) {
203         alert("Something went wrong...");
204     });
205 }
206
207 function continue_workflow() {
208     window.location.replace("/workflow/");
209 }
210
211 ///////////////////
212 //Class Definitions
213 ///////////////////
214
215 class MultipleSelectFilterWidget {
216
217     constructor(neighbors, items, initial) {
218         this.inputs = [];
219         this.graph_neighbors = neighbors;
220         this.filter_items = items;
221         this.currentLab = null;
222         this.available_resources = {};
223         this.result = {};
224         this.dropdown_count = 0;
225
226         for(let nodeId in this.filter_items) {
227             const node = this.filter_items[nodeId];
228             this.result[node.class] = {}
229         }
230
231         this.make_selection(initial);
232     }
233
234     make_selection(initial_data){
235         if(!initial_data || jQuery.isEmptyObject(initial_data))
236             return;
237
238         // Need to sort through labs first
239         let initial_lab = initial_data['lab'];
240         let initial_resources = initial_data['resource'];
241
242         for( let node_id in initial_lab) { // This should only be length one
243             const node = this.filter_items[node_id];
244             const selection_data = initial_lab[node_id];
245             if( selection_data.selected ) {
246                 this.select(node);
247                 this.markAndSweep(node);
248                 this.updateResult(node);
249             }
250             if(node['multiple']){
251                 this.make_multiple_selection(node, selection_data);
252             }
253             this.currentLab = node;
254             this.available_resources = JSON.parse(node['available_resources']);
255         }
256
257         for( let node_id in initial_resources){
258             const node = this.filter_items[node_id];
259             const selection_data = initial_resources[node_id];
260             if( selection_data.selected ) {
261                 this.select(node);
262                 this.markAndSweep(node);
263                 this.updateResult(node);
264             }
265             if(node['multiple']){
266                 this.make_multiple_selection(node, selection_data);
267             }
268         }
269         this.updateAvailibility();
270     }
271
272     make_multiple_selection(node, selection_data){
273         const prepop_data = selection_data.values;
274         for(let k in prepop_data){
275             const div = this.add_item_prepopulate(node, prepop_data[k]);
276             this.updateObjectResult(node, div.id, prepop_data[k]);
277         }
278     }
279
280     markAndSweep(root){
281         for(let i in this.filter_items) {
282             const node = this.filter_items[i];
283             node['marked'] = true; //mark all nodes
284         }
285
286         const toCheck = [root];
287         while(toCheck.length > 0){
288             const node = toCheck.pop();
289
290             if(!node['marked']) {
291                 continue; //already visited, just continue
292             }
293
294             node['marked'] = false; //mark as visited
295             if(node['follow'] || node == root){ //add neighbors if we want to follow this node
296                 const neighbors = this.graph_neighbors[node.id];
297                 for(let neighId of neighbors) {
298                     const neighbor = this.filter_items[neighId];
299                     toCheck.push(neighbor);
300                 }
301             }
302         }
303
304         //now remove all nodes still marked
305         for(let i in this.filter_items){
306             const node = this.filter_items[i];
307             if(node['marked']){
308                 this.disable_node(node);
309             }
310         }
311     }
312
313     process(node) {
314         if(node['selected']) {
315             this.markAndSweep(node);
316         }
317         else {  //TODO: make this not dumb
318             const selected = []
319             //remember the currently selected, then reset everything and reselect one at a time
320             for(let nodeId in this.filter_items) {
321                 node = this.filter_items[nodeId];
322                 if(node['selected']) {
323                     selected.push(node);
324                 }
325                 this.clear(node);
326             }
327             for(let node of selected) {
328                 this.select(node);
329                 this.markAndSweep(node);
330             }
331         }
332     }
333
334     select(node) {
335         const elem = document.getElementById(node['id']);
336         node['selected'] = true;
337         elem.classList.remove('bg-white', 'not-allowed', 'bg-light');
338         elem.classList.add('selected_node');
339
340         if(node['class'] == 'resource')
341             this.reserveResource(node);
342
343     }
344
345     clear(node) {
346         const elem = document.getElementById(node['id']);
347         node['selected'] = false;
348         node['selectable'] = true;
349         elem.classList.add('bg-white')
350         elem.classList.remove('not-allowed', 'bg-light', 'selected_node');
351     }
352
353     disable_node(node) {
354         const elem = document.getElementById(node['id']);
355         node['selected'] = false;
356         node['selectable'] = false;
357         elem.classList.remove('bg-white', 'selected_node');
358         elem.classList.add('not-allowed', 'bg-light');
359     }
360
361     labCheck(node){
362         // if lab is not already selected update available resources
363         if(!node['selected']) {
364             this.currentLab = node;
365             this.available_resources = JSON.parse(node['available_resources']);
366             this.updateAvailibility();
367         } else {
368             // a lab is already selected, clear already selected resources
369             if(confirm('Unselecting a lab will reset all selected resources, are you sure?')) {
370                 location.reload();
371                 return false;
372             }
373         }
374         return true;
375     }
376
377     updateAvailibility() {
378         const lab_resources = this.graph_neighbors[this.currentLab.id];
379
380         // need to loop through and update all quantities
381         for(let i in lab_resources) {
382             const resource_node = this.filter_items[lab_resources[i]];
383             const required_resources = JSON.parse(resource_node['required_resources']);
384             let elem = document.getElementById(resource_node.id).getElementsByClassName("grid-item-description")[0];
385             let leastAvailable = 100;
386             let currCount;
387             let quantityDescription;
388             let quantityNode;
389
390             for(let resource in required_resources) {
391                 currCount = Math.floor(this.available_resources[resource] / required_resources[resource]);
392                 if(currCount < leastAvailable)
393                     leastAvailable = currCount;
394
395                 if(!currCount || currCount < 0) {
396                     leastAvailable = 0
397                     break;
398                 }
399             }
400
401             if (elem.children[0]){
402                 elem.removeChild(elem.children[0]);
403             }
404
405             quantityDescription = '<br> Quantity Currently Available: ' + leastAvailable;
406             quantityNode = document.createElement('P');
407             if (leastAvailable > 0) {
408                 quantityDescription = quantityDescription.fontcolor('green');
409             } else {
410                 quantityDescription = quantityDescription.fontcolor('red');
411             }
412
413             quantityNode.innerHTML = quantityDescription;
414             elem.appendChild(quantityNode)
415         }
416     }
417
418     reserveResource(node){
419         const required_resources = JSON.parse(node['required_resources']);
420         let hostname = document.getElementById('id_hostname');
421         let image = document.getElementById('id_image');
422         let cnt = 0
423
424
425         for(let resource in required_resources){
426             this.available_resources[resource] -= required_resources[resource];
427             cnt += required_resources[resource];
428         }
429
430         if (cnt > 1 && hostname) {
431             hostname.readOnly = true;
432             // we only disable hostname modification because there is no sane case where you want all hosts to have the same hostname
433             // image is still allowed to be set across all hosts, but is filtered to the set of images that are commonly applicable still
434             // if no images exist that would apply to all hosts in a pod, then the user is restricted to not setting an image
435             // and the default image for each host is used
436         }
437
438         this.updateAvailibility();
439     }
440
441     releaseResource(node){
442         const required_resources = JSON.parse(node['required_resources']);
443         let hostname = document.getElementById('id_hostname');
444         let image = document.getElementById('id_image');
445
446         for(let resource in required_resources){
447             this.available_resources[resource] += required_resources[resource];
448         }
449
450         if (hostname && image) {
451             hostname.readOnly = false;
452             image.disabled = false;
453         }
454
455         this.updateAvailibility();
456     }
457
458     processClick(id){
459         let lab_check;
460         const node = this.filter_items[id];
461         if(!node['selectable'])
462             return;
463
464         // If they are selecting a lab, update accordingly
465         if (node['class'] == 'lab') {
466             lab_check = this.labCheck(node);
467             if (!lab_check)
468                 return;
469         }
470
471         // Can only select a resource if a lab is selected
472         if (!this.currentLab) {
473             alert('You must select a lab before selecting a resource');
474             return;
475         }
476
477         if(node['multiple']){
478             return this.processClickMultiple(node);
479         } else {
480             return this.processClickSingle(node);
481         }
482     }
483
484     processClickSingle(node){
485         node['selected'] = !node['selected']; //toggle on click
486         if(node['selected']) {
487             this.select(node);
488         } else {
489             this.clear(node);
490             this.releaseResource(node); // can't do this in clear since clear removes border
491         }
492         this.process(node);
493         this.updateResult(node);
494     }
495
496     processClickMultiple(node){
497         this.select(node);
498         const div = this.add_item_prepopulate(node, false);
499         this.process(node);
500         this.updateObjectResult(node, div.id, "");
501     }
502
503     restrictchars(input){
504         if( input.validity.patternMismatch ){
505             input.setCustomValidity("Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed");
506             input.reportValidity();
507         }
508         input.value = input.value.replace(/([^A-Za-z0-9-_.])+/g, "");
509         this.checkunique(input);
510     }
511
512     checkunique(tocheck){ //TODO: use set
513         const val = tocheck.value;
514         for( let input of this.inputs ){
515             if( input.value == val && input != tocheck){
516                 tocheck.setCustomValidity("All hostnames must be unique");
517                 tocheck.reportValidity();
518                 return;
519             }
520         }
521         tocheck.setCustomValidity("");
522     }
523
524     make_remove_button(div, node){
525         const button = document.createElement("BUTTON");
526         button.type = "button";
527         button.appendChild(document.createTextNode("Remove"));
528         button.classList.add("btn", "btn-danger", "d-inline-block");
529         const that = this;
530         button.onclick = function(){ that.remove_dropdown(div.id, node.id); }
531         return button;
532     }
533
534     make_input(div, node, prepopulate){
535         const input = document.createElement("INPUT");
536         input.type = node.form.type;
537         input.name = node.id + node.form.name
538         input.classList.add("form-control", "w-auto", "d-inline-block");
539         input.pattern = "(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})";
540         input.title = "Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed"
541         input.placeholder = node.form.placeholder;
542         this.inputs.push(input);
543         const that = this;
544         input.onchange = function() { that.updateObjectResult(node, div.id, input.value); that.restrictchars(this); };
545         input.oninput = function() { that.restrictchars(this); };
546         if(prepopulate)
547             input.value = prepopulate;
548         return input;
549     }
550
551     add_item_prepopulate(node, prepopulate){
552         const div = document.createElement("DIV");
553         div.id = "dropdown_" + this.dropdown_count;
554         div.classList.add("card", "flex-row", "d-flex", "mb-2");
555         this.dropdown_count++;
556         const label = document.createElement("H5")
557         label.appendChild(document.createTextNode(node['name']))
558         label.classList.add("p-1", "m-1", "flex-grow-1");
559         div.appendChild(label);
560         let remove_btn = this.make_remove_button(div, node);
561         remove_btn.classList.add("p-1", "m-1");
562         div.appendChild(remove_btn);
563         document.getElementById("dropdown_wrapper").appendChild(div);
564         return div;
565     }
566
567     remove_dropdown(div_id, node_id){
568         const div = document.getElementById(div_id);
569         const node = this.filter_items[node_id]
570         const parent = div.parentNode;
571         div.parentNode.removeChild(div);
572         this.result[node.class][node.id]['count']--;
573         this.releaseResource(node); // This can't be done on clear b/c clear removes border
574
575         //checks if we have removed last item in class
576         if(this.result[node.class][node.id]['count'] == 0){
577             delete this.result[node.class][node.id];
578             this.clear(node);
579         }
580     }
581
582     updateResult(node){
583         if(!node['multiple']){
584             this.result[node.class][node.id] = {selected: node.selected, id: node.model_id}
585             if(!node.selected)
586                 delete this.result[node.class][node.id];
587         }
588     }
589
590     updateObjectResult(node, childKey, childValue){
591         if(!this.result[node.class][node.id])
592             this.result[node.class][node.id] = {selected: true, id: node.model_id, count: 0}
593
594         this.result[node.class][node.id]['count']++;
595     }
596
597     finish(){
598         document.getElementById("filter_field").value = JSON.stringify(this.result);
599     }
600 }
601
602 class NetworkStep {
603     // expects:
604     //
605     // debug: bool
606     // resources: {
607     //     id: {
608     //         id: int,
609     //         value: {
610     //             description: string,
611     //         },
612     //         interfaces: [
613     //             id: int,
614     //             name: str,
615     //             description: str,
616     //             connections: [
617     //                 {
618     //                     network: int, [networks.id]
619     //                     tagged: bool
620     //                 }
621     //             ],
622     //         ],
623     //     }
624     // }
625     // networks: {
626     //     id: {
627     //         id: int,
628     //         name: str,
629     //         public: bool,
630     //     }
631     // }
632     //
633     constructor(debug, resources, networks, graphContainer, overviewContainer, toolbarContainer){
634         if(!this.check_support()) {
635             console.log("Aborting, browser is not supported");
636             return;
637         }
638
639         this.currentWindow = null;
640         this.netCount = 0;
641         this.netColors = ['red', 'blue', 'purple', 'green', 'orange', '#8CCDF5', '#1E9BAC'];
642         this.hostCount = 0;
643         this.lastHostBottom = 100;
644         this.networks = new Set();
645         this.has_public_net = false;
646         this.debug = debug;
647         this.editor = new mxEditor();
648         this.graph = this.editor.graph;
649
650         window.global_graph = this.graph;
651         window.network_rr_index = 5;
652
653         this.editor.setGraphContainer(graphContainer);
654         this.doGlobalConfig();
655
656         let mx_networks = {}
657
658         for(const network_id in networks) {
659             let network = networks[network_id];
660
661             mx_networks[network_id] = this.populateNetwork(network);
662         }
663
664         this.prefillHosts(resources, mx_networks);
665
666         //this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', '', "/static/img/mxgraph/zoom_in.png", true);
667         //this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', '', "/static/img/mxgraph/zoom_out.png", true);
668         this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', 'fa-search-plus');
669         this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', 'fa-search-minus');
670
671         if(this.debug){
672             this.editor.addAction('printXML', function(editor, cell) {
673                 mxLog.write(this.encodeGraph());
674                 mxLog.show();
675             }.bind(this));
676             this.addToolbarButton(this.editor, toolbarContainer, 'printXML', 'fa-file-code');
677         }
678
679         new mxOutline(this.graph, overviewContainer);
680         //sets the edge color to be the same as the network
681         this.graph.addListener(mxEvent.CELL_CONNECTED, function(sender, event) {this.cellConnectionHandler(sender, event)}.bind(this));
682         //hooks up double click functionality
683         this.graph.dblClick = function(evt, cell) {this.doubleClickHandler(evt, cell);}.bind(this);
684     }
685
686     check_support(){
687         if (!mxClient.isBrowserSupported()) {
688             mxUtils.error('Browser is not supported', 200, false);
689             return false;
690         }
691         return true;
692     }
693
694     /**
695      * Expects
696      * mx_interface: mxCell for the interface itself
697      * network: mxCell for the outer network
698      * tagged: bool
699      */
700     connectNetwork(mx_interface, network, tagged) {
701         var cell = new mxCell(
702             "connection from " + network + " to " + mx_interface,
703             new mxGeometry(0, 0, 50, 50));
704         cell.edge = true;
705         cell.geometry.relative = true;
706         cell.setValue(JSON.stringify({tagged: tagged}));
707
708         let terminal = this.getClosestNetworkCell(mx_interface.geometry.y, network);
709         let edge = this.graph.addEdge(cell, null, mx_interface, terminal);
710         this.colorEdge(edge, terminal, true);
711         this.graph.refresh(edge);
712     }
713
714     /**
715      * Expects:
716      *
717      * to: desired y axis position of the matching cell
718      * within: graph cell for a full network, with all child cells
719      *
720      * Returns:
721      * an mx cell, the one vertically closest to the desired value
722      *
723      * Side effect:
724      * modifies the <rr_index> on the <within> parameter
725      */
726     getClosestNetworkCell(to, within) {
727         if(window.network_rr_index === undefined) {
728             window.network_rr_index = 5;
729         }
730
731         let child_keys = within.children.keys();
732         let children = Array.from(within.children);
733         let index = (window.network_rr_index++) % children.length;
734
735         let child = within.children[child_keys[index]];
736
737         return children[index];
738     }
739
740     /** Expects
741      *
742      * hosts: {
743      *     id: {
744      *         id: int,
745      *         value: {
746      *             description: string,
747      *         },
748      *         interfaces: [
749      *             id: int,
750      *             name: str,
751      *             description: str,
752      *             connections: [
753      *                 {
754      *                     network: int, [networks.id]
755      *                     tagged: bool 
756      *                 }
757      *             ],
758      *         ],
759      *     }
760      * }
761      *
762      * network_mappings: {
763      *     <django network id>: <mxnetwork id>
764      * }
765      *
766      * draws given hosts into the mxgraph
767      */
768     prefillHosts(hosts, network_mappings){
769         for(const host_id in hosts) {
770             this.makeHost(hosts[host_id], network_mappings);
771         }
772     }
773
774     cellConnectionHandler(sender, event){
775         const edge = event.getProperty('edge');
776         const terminal = event.getProperty('terminal')
777         const source = event.getProperty('source');
778         if(this.checkAllowed(edge, terminal, source)) {
779             this.colorEdge(edge, terminal, source);
780             this.alertVlan(edge, terminal, source);
781         }
782     }
783
784     doubleClickHandler(evt, cell) {
785         if( cell != null ){
786             if( cell.getParent() != null && cell.getParent().getId().indexOf("network") > -1) {
787                 cell = cell.getParent();
788             }
789             if( cell.isEdge() || cell.getId().indexOf("network") > -1 ) {
790                 this.createDeleteDialog(cell.getId());
791             }
792             else {
793                 this.showDetailWindow(cell);
794            }
795         }
796     }
797
798     alertVlan(edge, terminal, source) {
799         if( terminal == null || edge.getTerminal(!source) == null) {
800             return;
801         }
802         const form = document.createElement("form");
803         const tagged = document.createElement("input");
804         tagged.type = "radio";
805         tagged.name = "tagged";
806         tagged.value = "True";
807         tagged.checked = "True";
808         form.appendChild(tagged);
809         form.appendChild(document.createTextNode(" Tagged"));
810         form.appendChild(document.createElement("br"));
811
812         const untagged = document.createElement("input");
813         untagged.type = "radio";
814         untagged.name = "tagged";
815         untagged.value = "False";
816         form.appendChild(untagged);
817         form.appendChild(document.createTextNode(" Untagged"));
818         form.appendChild(document.createElement("br"));
819
820         const yes_button = document.createElement("button");
821         yes_button.onclick = function() {this.parseVlanWindow(edge.getId());}.bind(this);
822         yes_button.appendChild(document.createTextNode("Okay"));
823
824         const cancel_button = document.createElement("button");
825         cancel_button.onclick = function() {this.deleteVlanWindow(edge.getId());}.bind(this);
826         cancel_button.appendChild(document.createTextNode("Cancel"));
827
828         const error_div = document.createElement("div");
829         error_div.id = "current_window_errors";
830         form.appendChild(error_div);
831
832         const content = document.createElement('div');
833         content.appendChild(form);
834         content.appendChild(yes_button);
835         content.appendChild(cancel_button);
836         this.showWindow("Vlan Selection", content, 200, 200);
837     }
838
839     createDeleteDialog(id) {
840         const content = document.createElement('div');
841         const remove_button = document.createElement("button");
842         remove_button.style.width = '46%';
843         remove_button.onclick = function() { this.deleteCell(id);}.bind(this);
844         remove_button.appendChild(document.createTextNode("Remove"));
845         const cancel_button = document.createElement("button");
846         cancel_button.style.width = '46%';
847         cancel_button.onclick = function() { this.closeWindow();}.bind(this);
848         cancel_button.appendChild(document.createTextNode("Cancel"));
849
850         content.appendChild(remove_button);
851         content.appendChild(cancel_button);
852         this.showWindow('Do you want to delete this network?', content, 200, 62);
853     }
854
855     checkAllowed(edge, terminal, source) {
856         //check if other terminal is null, and that they are different
857         const otherTerminal = edge.getTerminal(!source);
858         if(terminal != null && otherTerminal != null) {
859             if( terminal.getParent().getId().split('_')[0] == //'host' or 'network'
860                 otherTerminal.getParent().getId().split('_')[0] ) {
861                 //not allowed
862                 this.graph.removeCells([edge]);
863                 return false;
864             }
865         }
866         return true;
867     }
868
869     colorEdge(edge, terminal, source) {
870         if(terminal.getParent().getId().indexOf('network') >= 0) {
871             const styles = terminal.getParent().getStyle().split(';');
872             let color = 'black';
873             for(let style of styles){
874                 const kvp = style.split('=');
875                 if(kvp[0] == "fillColor"){
876                     color = kvp[1];
877                 }
878             }
879
880             edge.setStyle('strokeColor=' + color);
881         } else {
882             console.log("Failed to color " + edge + ", " + terminal + ", " + source);
883         }
884     }
885
886     showDetailWindow(cell) {
887         const info = JSON.parse(cell.getValue());
888         const content = document.createElement("div");
889         const pre_tag = document.createElement("pre");
890         pre_tag.appendChild(document.createTextNode("Name: " + info.name + "\nDescription:\n" + info.description));
891         const ok_button = document.createElement("button");
892         ok_button.onclick = function() { this.closeWindow();};
893         content.appendChild(pre_tag);
894         content.appendChild(ok_button);
895         this.showWindow('Details', content, 400, 400);
896     }
897
898     restoreFromXml(xml, editor) {
899         const doc = mxUtils.parseXml(xml);
900         const node = doc.documentElement;
901         editor.readGraphModel(node);
902
903         //Iterate over all children, and parse the networks to add them to the sidebar
904         for( const cell of this.graph.getModel().getChildren(this.graph.getDefaultParent())) {
905             if(cell.getId().indexOf("network") > -1) {
906                 const info = JSON.parse(cell.getValue());
907                 const name = info['name'];
908                 this.networks.add(name);
909                 const styles = cell.getStyle().split(";");
910                 let color = null;
911                 for(const style of styles){
912                     const kvp = style.split('=');
913                     if(kvp[0] == "fillColor") {
914                         color = kvp[1];
915                         break;
916                     }
917                 }
918                 if(info.public){
919                     this.has_public_net = true;
920                 }
921                 this.netCount++;
922                 this.makeSidebarNetwork(name, color, cell.getId());
923             }
924         }
925     }
926
927     deleteCell(cellId) {
928         var cell = this.graph.getModel().getCell(cellId);
929         if( cellId.indexOf("network") > -1 ) {
930             let elem = document.getElementById(cellId);
931             elem.parentElement.removeChild(elem);
932         }
933         this.graph.removeCells([cell]);
934         this.currentWindow.destroy();
935     }
936
937     newNetworkWindow() {
938         const input = document.createElement("input");
939         input.type = "text";
940         input.name = "net_name";
941         input.maxlength = 100;
942         input.id = "net_name_input";
943         input.style.margin = "5px";
944
945         const yes_button = document.createElement("button");
946         yes_button.onclick = function() {this.parseNetworkWindow();}.bind(this);
947         yes_button.appendChild(document.createTextNode("Okay"));
948
949         const cancel_button = document.createElement("button");
950         cancel_button.onclick = function() {this.closeWindow();}.bind(this);
951         cancel_button.appendChild(document.createTextNode("Cancel"));
952
953         const error_div = document.createElement("div");
954         error_div.id = "current_window_errors";
955
956         const content = document.createElement("div");
957         content.appendChild(document.createTextNode("Name: "));
958         content.appendChild(input);
959         content.appendChild(document.createElement("br"));
960         content.appendChild(yes_button);
961         content.appendChild(cancel_button);
962         content.appendChild(document.createElement("br"));
963         content.appendChild(error_div);
964
965         this.showWindow("Network Creation", content, 300, 300);
966     }
967
968     parseNetworkWindow() {
969         const net_name = document.getElementById("net_name_input").value
970         const error_div = document.getElementById("current_window_errors");
971         if( this.networks.has(net_name) ){
972             error_div.innerHTML = "All network names must be unique";
973             return;
974         }
975         this.addNetwork(net_name);
976         this.currentWindow.destroy();
977     }
978
979     addToolbarButton(editor, toolbar, action, image) {
980         const button = document.createElement('button');
981         button.setAttribute('class', 'btn btn-sm m-1');
982         if (image != null) {
983             const icon = document.createElement('i');
984             icon.setAttribute('class', 'fas ' + image);
985             button.appendChild(icon);
986         }
987         mxEvent.addListener(button, 'click', function(evt) {
988             editor.execute(action);
989         });
990         mxUtils.write(button, '');
991         toolbar.appendChild(button);
992     };
993
994     encodeGraph() {
995         const encoder = new mxCodec();
996         const xml = encoder.encode(this.graph.getModel());
997         return mxUtils.getXml(xml);
998     }
999
1000     doGlobalConfig() {
1001         //general graph stuff
1002         this.graph.setMultigraph(false);
1003         this.graph.setCellsSelectable(false);
1004         this.graph.setCellsMovable(false);
1005
1006         //testing
1007         this.graph.vertexLabelIsMovable = true;
1008
1009         //edge behavior
1010         this.graph.setConnectable(true);
1011         this.graph.setAllowDanglingEdges(false);
1012         mxEdgeHandler.prototype.snapToTerminals = true;
1013         mxConstants.MIN_HOTSPOT_SIZE = 16;
1014         mxConstants.DEFAULT_HOTSPOT = 1;
1015         //edge 'style' (still affects behavior greatly)
1016         const style = this.graph.getStylesheet().getDefaultEdgeStyle();
1017         style[mxConstants.STYLE_EDGE] = mxConstants.EDGESTYLE_ELBOW;
1018         style[mxConstants.STYLE_ENDARROW] = mxConstants.NONE;
1019         style[mxConstants.STYLE_ROUNDED] = true;
1020         style[mxConstants.STYLE_FONTCOLOR] = 'black';
1021         style[mxConstants.STYLE_STROKECOLOR] = 'red';
1022         style[mxConstants.STYLE_LABEL_BACKGROUNDCOLOR] = '#FFFFFF';
1023         style[mxConstants.STYLE_STROKEWIDTH] = '3';
1024         style[mxConstants.STYLE_ROUNDED] = true;
1025         style[mxConstants.STYLE_EDGE] = mxEdgeStyle.EntityRelation;
1026
1027         const hostStyle = this.graph.getStylesheet().getDefaultVertexStyle();
1028         hostStyle[mxConstants.STYLE_ROUNDED] = 1;
1029
1030         this.graph.convertValueToString = function(cell) {
1031             try{
1032                 //changes value for edges with xml value
1033                 if(cell.isEdge()) {
1034                     if(JSON.parse(cell.getValue())["tagged"]) {
1035                         return "tagged";
1036                     }
1037                     return "untagged";
1038                 }
1039                 else{
1040                     return JSON.parse(cell.getValue())['name'];
1041                 }
1042             }
1043             catch(e){
1044                 return cell.getValue();
1045             }
1046         };
1047     }
1048
1049     showWindow(title, content, width, height) {
1050         //create transparent black background
1051         const background = document.createElement('div');
1052         background.style.position = 'absolute';
1053         background.style.left = '0px';
1054         background.style.top = '0px';
1055         background.style.right = '0px';
1056         background.style.bottom = '0px';
1057         background.style.background = 'black';
1058         mxUtils.setOpacity(background, 50);
1059         document.body.appendChild(background);
1060
1061         const x = Math.max(0, document.body.scrollWidth/2-width/2);
1062         const y = Math.max(10, (document.body.scrollHeight ||
1063                     document.documentElement.scrollHeight)/2-height*2/3);
1064
1065         const wnd = new mxWindow(title, content, x, y, width, height, false, true);
1066         wnd.setClosable(false);
1067
1068         wnd.addListener(mxEvent.DESTROY, function(evt) {
1069             this.graph.setEnabled(true);
1070             mxEffects.fadeOut(background, 50, true, 10, 30, true);
1071         }.bind(this));
1072         this.currentWindow = wnd;
1073
1074         this.graph.setEnabled(false);
1075         this.currentWindow.setVisible(true);
1076     };
1077
1078     closeWindow() {
1079         //allows the current window to be destroyed
1080         this.currentWindow.destroy();
1081     };
1082
1083     othersUntagged(edgeID) {
1084         const edge = this.graph.getModel().getCell(edgeID);
1085         const end1 = edge.getTerminal(true);
1086         const end2 = edge.getTerminal(false);
1087
1088         if( end1.getParent().getId().split('_')[0] == 'host' ){
1089             var netint = end1;
1090         } else {
1091             var netint = end2;
1092         }
1093
1094         var edges = netint.edges;
1095         for( let edge of edges) {
1096             if( edge.getValue() ) {
1097                 var tagged = JSON.parse(edge.getValue()).tagged;
1098             } else {
1099                 var tagged = true;
1100             }
1101             if( !tagged ) {
1102                 return true;
1103             }
1104         }
1105
1106         return false;
1107     };
1108
1109
1110     deleteVlanWindow(edgeID) {
1111         const cell = this.graph.getModel().getCell(edgeID);
1112         this.graph.removeCells([cell]);
1113         this.currentWindow.destroy();
1114     }
1115
1116     parseVlanWindow(edgeID) {
1117         //do parsing and data manipulation
1118         const radios = document.getElementsByName("tagged");
1119         const edge = this.graph.getModel().getCell(edgeID);
1120
1121         for(let radio of radios){
1122             if(radio.checked) {
1123                 //set edge to be tagged or untagged
1124                 if( radio.value == "False") {
1125                     if( this.othersUntagged(edgeID) ) {
1126                         document.getElementById("current_window_errors").innerHTML = "Only one untagged vlan per interface is allowed.";
1127                         return;
1128                     }
1129                 }
1130                 const edgeVal = {tagged: radio.value == "True"};
1131                 edge.setValue(JSON.stringify(edgeVal));
1132                 break;
1133             }
1134         }
1135         this.graph.refresh(edge);
1136         this.closeWindow();
1137     }
1138
1139     makeMxNetwork(net_name, is_public = false) {
1140         const model = this.graph.getModel();
1141         const width = 10;
1142         const height = 1700;
1143         const xoff = 400 + (30 * this.netCount);
1144         const yoff = -10;
1145         let color = this.netColors[this.netCount];
1146         if( this.netCount > (this.netColors.length - 1)) {
1147             color = Math.floor(Math.random() * 16777215); //int in possible color space
1148             color = '#' + color.toString(16).toUpperCase(); //convert to hex
1149         }
1150         const net_val = { name: net_name, public: is_public};
1151         const net = this.graph.insertVertex(
1152             this.graph.getDefaultParent(),
1153             'network_' + this.netCount,
1154             JSON.stringify(net_val),
1155             xoff,
1156             yoff,
1157             width,
1158             height,
1159             'fillColor=' + color,
1160             false
1161         );
1162         const num_ports = 45;
1163         for(var i=0; i<num_ports; i++){
1164             let port = this.graph.insertVertex(
1165                 net,
1166                 null,
1167                 '',
1168                 0,
1169                 (1/num_ports) * i,
1170                 10,
1171                 height / num_ports,
1172                 'fillColor=black;opacity=0',
1173                 true
1174             );
1175         }
1176
1177         const ret_val = { color: color, element_id: "network_" + this.netCount };
1178
1179         this.networks.add(net_name);
1180         this.netCount++;
1181         return ret_val;
1182     }
1183
1184     // expects:
1185     //
1186     // {
1187     //     id: int,
1188     //     name: str,
1189     //     public: bool,
1190     // }
1191     //
1192     // returns:
1193     // mxgraph id of network
1194     populateNetwork(network) {
1195         let mxNet = this.makeMxNetwork(network.name, network.public);
1196         this.makeSidebarNetwork(network.name, mxNet.color, mxNet.element_id);
1197
1198         if( network.public ) {
1199             this.has_public_net = true;
1200         }
1201
1202         return mxNet.element_id;
1203     }
1204
1205     addPublicNetwork() {
1206         const net = this.makeMxNetwork("public", true);
1207         this.makeSidebarNetwork("public", net['color'], net['element_id']);
1208         this.has_public_net = true;
1209     }
1210
1211     addNetwork(net_name) {
1212         const ret = this.makeMxNetwork(net_name);
1213         this.makeSidebarNetwork(net_name, ret.color, ret.element_id);
1214     }
1215
1216     updateHosts(removed) {
1217         const cells = []
1218         for(const hostID of removed) {
1219             cells.push(this.graph.getModel().getCell("host_" + hostID));
1220         }
1221         this.graph.removeCells(cells);
1222
1223         const hosts = this.graph.getChildVertices(this.graph.getDefaultParent());
1224         let topdist = 100;
1225         for(const i in hosts) {
1226             const host = hosts[i];
1227             if(host.id.startsWith("host_")){
1228                 const geometry = host.getGeometry();
1229                 geometry.y = topdist + 50;
1230                 topdist = geometry.y + geometry.height;
1231                 host.setGeometry(geometry);
1232             }
1233         }
1234     }
1235
1236     makeSidebarNetwork(net_name, color, net_id){
1237         const colorBlob = document.createElement("div");
1238         colorBlob.className = "square-20 rounded-circle";
1239         colorBlob.style['background'] = color;
1240
1241         const textContainer = document.createElement("span");
1242         textContainer.className = "ml-2";
1243         textContainer.appendChild(document.createTextNode(net_name));
1244
1245         const timesIcon = document.createElement("i");
1246         timesIcon.classList.add("fas", "fa-times");
1247
1248         const deletebutton = document.createElement("button");
1249         deletebutton.className = "btn btn-danger ml-auto square-20 p-0 d-flex justify-content-center";
1250         deletebutton.appendChild(timesIcon);
1251         deletebutton.addEventListener("click", function() { this.createDeleteDialog(net_id); }.bind(this), false);
1252
1253         const newNet = document.createElement("li");
1254         newNet.classList.add("list-group-item", "d-flex", "bg-light");
1255         newNet.id = net_id;
1256         newNet.appendChild(colorBlob);
1257         newNet.appendChild(textContainer);
1258
1259         if( net_name != "public" ) {
1260             newNet.appendChild(deletebutton);
1261         }
1262         document.getElementById("network_list").appendChild(newNet);
1263     }
1264
1265     /** 
1266      * Expects format:
1267      * {
1268      *     'id': int,
1269      *     'value': {
1270      *         'description': string,
1271      *     },
1272      *     'interfaces': [
1273      *          {
1274      *              id: int,
1275      *              name: str,
1276      *              description: str,
1277      *              connections: [
1278      *                  {
1279      *                      network: int, <django network id>,
1280      *                      tagged: bool
1281      *                  }
1282      *              ]
1283      *          }
1284      *      ]
1285      * }
1286      *
1287      * network_mappings: {
1288      *     <django network id>: <mxnetwork id>
1289      * }
1290      */
1291     makeHost(hostInfo, network_mappings) {
1292         const value = JSON.stringify(hostInfo['value']);
1293         const interfaces = hostInfo['interfaces'];
1294         const width = 100;
1295         const height = (25 * interfaces.length) + 25;
1296         const xoff = 75;
1297         const yoff = this.lastHostBottom + 50;
1298         this.lastHostBottom = yoff + height;
1299         const host = this.graph.insertVertex(
1300             this.graph.getDefaultParent(),
1301             'host_' + hostInfo['id'],
1302             value,
1303             xoff,
1304             yoff,
1305             width,
1306             height,
1307             'editable=0',
1308             false
1309         );
1310         host.getGeometry().offset = new mxPoint(-50,0);
1311         host.setConnectable(false);
1312         this.hostCount++;
1313
1314         for(var i=0; i<interfaces.length; i++) {
1315             const port = this.graph.insertVertex(
1316                 host,
1317                 null,
1318                 JSON.stringify(interfaces[i]),
1319                 90,
1320                 (i * 25) + 12,
1321                 20,
1322                 20,
1323                 'fillColor=blue;editable=0',
1324                 false
1325             );
1326             port.getGeometry().offset = new mxPoint(-4*interfaces[i].name.length -2,0);
1327             const iface = interfaces[i];
1328             for( const connection of iface.connections ) {
1329                 const network = this
1330                     .graph
1331                     .getModel()
1332                     .getCell(network_mappings[connection.network]);
1333
1334                 this.connectNetwork(port, network, connection.tagged);
1335             }
1336             this.graph.refresh(port);
1337         }
1338         this.graph.refresh(host);
1339     }
1340
1341     prepareForm() {
1342         const input_elem = document.getElementById("hidden_xml_input");
1343         input_elem.value = this.encodeGraph(this.graph);
1344     }
1345 }
1346
1347 class SearchableSelectMultipleWidget {
1348     constructor(format_vars, field_dataset, field_initial) {
1349         this.format_vars = format_vars;
1350         this.items = field_dataset;
1351         this.initial = field_initial;
1352
1353         this.expanded_name_trie = {"isComplete": false};
1354         this.small_name_trie = {"isComplete": false};
1355         this.string_trie = {"isComplete": false};
1356
1357         this.added_items = new Set();
1358
1359         for( let e of ["show_from_noentry", "show_x_results", "results_scrollable", "selectable_limit", "placeholder"] )
1360         {
1361             this[e] = format_vars[e];
1362         }
1363
1364         this.search_field_init();
1365
1366         if( this.show_from_noentry )
1367         {
1368             this.search("");
1369         }
1370     }
1371
1372     disable() {
1373         const textfield = document.getElementById("user_field");
1374         const drop = document.getElementById("drop_results");
1375
1376         textfield.disabled = "True";
1377         drop.style.display = "none";
1378
1379         const btns = document.getElementsByClassName("btn-remove");
1380         for( const btn of btns )
1381         {
1382             btn.classList.add("disabled");
1383             btn.onclick = "";
1384         }
1385     }
1386
1387     search_field_init() {
1388         this.build_all_tries(this.items);
1389
1390         for( const elem of this.initial )
1391         {
1392             this.select_item(elem);
1393         }
1394         if(this.initial.length == 1)
1395         {
1396             this.search(this.items[this.initial[0]]["small_name"]);
1397             document.getElementById("user_field").value = this.items[this.initial[0]]["small_name"];
1398         }
1399     }
1400
1401     build_all_tries(dict)
1402     {
1403         for( const key in dict )
1404         {
1405             this.add_item(dict[key]);
1406         }
1407     }
1408
1409     add_item(item)
1410     {
1411         const id = item['id'];
1412         this.add_to_tree(item['expanded_name'], id, this.expanded_name_trie);
1413         this.add_to_tree(item['small_name'], id, this.small_name_trie);
1414         this.add_to_tree(item['string'], id, this.string_trie);
1415     }
1416
1417     add_to_tree(str, id, trie)
1418     {
1419         let inner_trie = trie;
1420         while( str )
1421         {
1422             if( !inner_trie[str.charAt(0)] )
1423             {
1424                 var new_trie = {};
1425                 inner_trie[str.charAt(0)] = new_trie;
1426             }
1427             else
1428             {
1429                 var new_trie = inner_trie[str.charAt(0)];
1430             }
1431
1432             if( str.length == 1 )
1433             {
1434                 new_trie.isComplete = true;
1435                 if( !new_trie.ids )
1436                 {
1437                     new_trie.ids = [];
1438                 }
1439                 new_trie.ids.push(id);
1440             }
1441             inner_trie = new_trie;
1442             str = str.substring(1);
1443         }
1444     }
1445
1446     search(input)
1447     {
1448         if( input.length == 0 && !this.show_from_noentry){
1449             this.dropdown([]);
1450             return;
1451         }
1452         else if( input.length == 0 && this.show_from_noentry)
1453         {
1454             this.dropdown(this.items); //show all items
1455         }
1456         else
1457         {
1458             const trees = []
1459             const tr1 = this.getSubtree(input, this.expanded_name_trie);
1460             trees.push(tr1);
1461             const tr2 = this.getSubtree(input, this.small_name_trie);
1462             trees.push(tr2);
1463             const tr3 = this.getSubtree(input, this.string_trie);
1464             trees.push(tr3);
1465             const results = this.collate(trees);
1466             this.dropdown(results);
1467         }
1468     }
1469
1470     getSubtree(input, given_trie)
1471     {
1472         /*
1473         recursive function to return the trie accessed at input
1474         */
1475
1476         if( input.length == 0 ){
1477             return given_trie;
1478         }
1479
1480         else{
1481             const substr = input.substring(0, input.length - 1);
1482             const last_char = input.charAt(input.length-1);
1483             const subtrie = this.getSubtree(substr, given_trie);
1484
1485             if( !subtrie ) //substr not in the trie
1486             {
1487                 return {};
1488             }
1489
1490             const indexed_trie = subtrie[last_char];
1491             return indexed_trie;
1492         }
1493     }
1494
1495     serialize(trie)
1496     {
1497         /*
1498         takes in a trie and returns a list of its item id's
1499         */
1500         let itemIDs = [];
1501         if ( !trie )
1502         {
1503             return itemIDs; //empty, base case
1504         }
1505         for( const key in trie )
1506         {
1507             if(key.length > 1)
1508             {
1509                 continue;
1510             }
1511             itemIDs = itemIDs.concat(this.serialize(trie[key]));
1512         }
1513         if ( trie.isComplete )
1514         {
1515             itemIDs.push(...trie.ids);
1516         }
1517
1518         return itemIDs;
1519     }
1520
1521     collate(trees)
1522     {
1523         /*
1524         takes a list of tries
1525         returns a list of ids of objects that are available
1526         */
1527         const results = [];
1528         for( const tree of trees )
1529         {
1530             const available_IDs = this.serialize(tree);
1531
1532             for( const itemID of available_IDs ) {
1533                 results[itemID] = this.items[itemID];
1534             }
1535         }
1536         return results;
1537     }
1538
1539     generate_element_text(obj)
1540     {
1541         const content_strings = [obj.expanded_name, obj.small_name, obj.string].filter(x => Boolean(x));
1542         const result = content_strings.shift();
1543         if( result == null || content_strings.length < 1) {
1544             return result;
1545         } else {
1546             return result + " (" + content_strings.join(", ") + ")";
1547         }
1548     }
1549
1550     dropdown(ids)
1551     {
1552         /*
1553         takes in a mapping of ids to objects in  items
1554         and displays them in the dropdown
1555         */
1556         const drop = document.getElementById("drop_results");
1557         while(drop.firstChild)
1558         {
1559             drop.removeChild(drop.firstChild);
1560         }
1561
1562         for( const id in ids )
1563         {
1564             const obj = this.items[id];
1565             const result_text = this.generate_element_text(obj);
1566             const result_entry = document.createElement("a");
1567             result_entry.href = "#";
1568             result_entry.innerText = result_text;
1569             result_entry.title = result_text;
1570             result_entry.classList.add("list-group-item", "list-group-item-action", "overflow-ellipsis", "flex-shrink-0");
1571             result_entry.onclick = function() { searchable_select_multiple_widget.select_item(obj.id); };
1572             const tooltip = document.createElement("span");
1573             const tooltiptext = document.createTextNode(result_text);
1574             tooltip.appendChild(tooltiptext);
1575             tooltip.classList.add("d-none");
1576             result_entry.appendChild(tooltip);
1577             drop.appendChild(result_entry);
1578         }
1579
1580         const scroll_restrictor = document.getElementById("scroll_restrictor");
1581
1582         if( !drop.firstChild )
1583         {
1584             scroll_restrictor.style.visibility = 'hidden';
1585         }
1586         else
1587         {
1588             scroll_restrictor.style.visibility = 'inherit';
1589         }
1590     }
1591
1592     select_item(item_id)
1593     {
1594         if( (this.selectable_limit > -1 && this.added_items.size < this.selectable_limit) || this.selectable_limit < 0 )
1595         {
1596             this.added_items.add(item_id);
1597         }
1598         this.update_selected_list();
1599         // clear search bar contents
1600         document.getElementById("user_field").value = "";
1601         document.getElementById("user_field").focus();
1602         this.search("");
1603     }
1604
1605     remove_item(item_id)
1606     {
1607         this.added_items.delete(item_id);
1608
1609         this.update_selected_list()
1610         document.getElementById("user_field").focus();
1611     }
1612
1613     update_selected_list()
1614     {
1615         document.getElementById("added_number").innerText = this.added_items.size;
1616         const selector = document.getElementById('selector');
1617         selector.value = JSON.stringify([...this.added_items]);
1618         const added_list = document.getElementById('added_list');
1619
1620         while(selector.firstChild)
1621         {
1622             selector.removeChild(selector.firstChild);
1623         }
1624         while(added_list.firstChild)
1625         {
1626             added_list.removeChild(added_list.firstChild);
1627         }
1628
1629         const list_html = document.createElement("div");
1630         list_html.classList.add("list-group");
1631
1632         for( const item_id of this.added_items )
1633         {
1634             const times = document.createElement("li");
1635             times.classList.add("fas", "fa-times");
1636
1637             const deleteButton = document.createElement("a");
1638             deleteButton.href = "#";
1639             deleteButton.innerHTML = "<i class='fas fa-times'></i>"
1640             // Setting .onclick/.addEventListener does not work,
1641             // which is why I took the setAttribute approach
1642             // If anyone knows why, please let me know :]
1643             deleteButton.setAttribute("onclick", `searchable_select_multiple_widget.remove_item(${item_id});`);
1644             deleteButton.classList.add("btn");
1645             const deleteColumn = document.createElement("div");
1646             deleteColumn.classList.add("col-auto");
1647             deleteColumn.append(deleteButton);
1648
1649             const item = this.items[item_id];
1650             const element_entry_text = this.generate_element_text(item);
1651             const textColumn = document.createElement("div");
1652             textColumn.classList.add("col", "overflow-ellipsis");
1653             textColumn.innerText = element_entry_text;
1654             textColumn.title = element_entry_text;
1655
1656             const itemRow = document.createElement("div");
1657             itemRow.classList.add("list-group-item", "d-flex", "p-0", "align-items-center");
1658             itemRow.append(textColumn, deleteColumn);
1659
1660             list_html.append(itemRow);
1661         }
1662         added_list.innerHTML = list_html.innerHTML;
1663     }
1664 }