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