5 form_submission_callbacks = []; //all runnables will be executed before form submission
12 function updatePage(data){
13 updateBreadcrumbs(data['meta']);
14 $("formContainer").html(data['content']);
17 function submitStepForm(next_step = "current"){
19 const step_form_data = $("#step_form").serialize();
20 const form_data = $.param({
22 "step_form": step_form_data,
23 "csrfmiddlewaretoken": $("[name=csrfmiddlewaretoken]").val()
25 console.log(form_data);
29 (data) => updatePage(data),
31 ).fail(() => alert("failure"));
34 function run_form_callbacks(){
35 for(f of form_submission_callbacks)
37 form_submission_callbacks = [];
44 class MultipleSelectFilterWidget {
46 constructor(neighbors, items, initial) {
48 this.graph_neighbors = neighbors;
49 this.filter_items = items;
51 this.dropdown_count = 0;
53 for(let nodeId in this.filter_items) {
54 const node = this.filter_items[nodeId];
55 this.result[node.class] = {}
58 this.make_selection(initial);
61 make_selection( initial_data ){
62 if(!initial_data || jQuery.isEmptyObject(initial_data))
64 for(let item_class in initial_data) {
65 const selected_items = initial_data[item_class];
66 for( let node_id in selected_items ){
67 const node = this.filter_items[node_id];
68 const selection_data = selected_items[node_id]
69 if( selection_data.selected ) {
71 this.markAndSweep(node);
72 this.updateResult(node);
75 this.make_multiple_selection(node, selection_data);
81 make_multiple_selection(node, selection_data){
82 const prepop_data = selection_data.values;
83 for(let k in prepop_data){
84 const div = this.add_item_prepopulate(node, prepop_data[k]);
85 this.updateObjectResult(node, div.id, prepop_data[k]);
90 for(let i in this.filter_items) {
91 const node = this.filter_items[i];
92 node['marked'] = true; //mark all nodes
95 const toCheck = [root];
96 while(toCheck.length > 0){
97 const node = toCheck.pop();
99 continue; //already visited, just continue
101 node['marked'] = false; //mark as visited
102 if(node['follow'] || node == root){ //add neighbors if we want to follow this node
103 const neighbors = this.graph_neighbors[node.id];
104 for(let neighId of neighbors) {
105 const neighbor = this.filter_items[neighId];
106 toCheck.push(neighbor);
111 //now remove all nodes still marked
112 for(let i in this.filter_items){
113 const node = this.filter_items[i];
115 this.disable_node(node);
121 if(node['selected']) {
122 this.markAndSweep(node);
124 else { //TODO: make this not dumb
126 //remember the currently selected, then reset everything and reselect one at a time
127 for(let nodeId in this.filter_items) {
128 node = this.filter_items[nodeId];
129 if(node['selected']) {
134 for(let node of selected) {
136 this.markAndSweep(node);
142 const elem = document.getElementById(node['id']);
143 node['selected'] = true;
144 elem.classList.remove('bg-white', 'not-allowed', 'bg-light');
145 elem.classList.add('selected_node');
149 const elem = document.getElementById(node['id']);
150 node['selected'] = false;
151 node['selectable'] = true;
152 elem.classList.add('bg-white')
153 elem.classList.remove('not-allowed', 'bg-light', 'selected_node');
157 const elem = document.getElementById(node['id']);
158 node['selected'] = false;
159 node['selectable'] = false;
160 elem.classList.remove('bg-white', 'selected_node');
161 elem.classList.add('not-allowed', 'bg-light');
165 const node = this.filter_items[id];
166 if(!node['selectable'])
169 if(node['multiple']){
170 return this.processClickMultiple(node);
172 return this.processClickSingle(node);
176 processClickSingle(node){
177 node['selected'] = !node['selected']; //toggle on click
178 if(node['selected']) {
184 this.updateResult(node);
187 processClickMultiple(node){
189 const div = this.add_item_prepopulate(node, false);
191 this.updateObjectResult(node, div.id, "");
194 restrictchars(input){
195 if( input.validity.patternMismatch ){
196 input.setCustomValidity("Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed");
197 input.reportValidity();
199 input.value = input.value.replace(/([^A-Za-z0-9-_.])+/g, "");
200 this.checkunique(input);
203 checkunique(tocheck){ //TODO: use set
204 const val = tocheck.value;
205 for( let input of this.inputs ){
206 if( input.value == val && input != tocheck){
207 tocheck.setCustomValidity("All hostnames must be unique");
208 tocheck.reportValidity();
212 tocheck.setCustomValidity("");
215 make_remove_button(div, node){
216 const button = document.createElement("BUTTON");
217 button.type = "button";
218 button.appendChild(document.createTextNode("Remove"));
219 button.classList.add("btn", "btn-danger", "d-inline-block");
221 button.onclick = function(){ that.remove_dropdown(div.id, node.id); }
225 make_input(div, node, prepopulate){
226 const input = document.createElement("INPUT");
227 input.type = node.form.type;
228 input.name = node.id + node.form.name
229 input.classList.add("form-control", "w-auto", "d-inline-block");
230 input.pattern = "(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})";
231 input.title = "Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed"
232 input.placeholder = node.form.placeholder;
233 this.inputs.push(input);
235 input.onchange = function() { that.updateObjectResult(node, div.id, input.value); that.restrictchars(this); };
236 input.oninput = function() { that.restrictchars(this); };
238 input.value = prepopulate;
242 add_item_prepopulate(node, prepopulate){
243 const div = document.createElement("DIV");
244 div.id = "dropdown_" + this.dropdown_count;
245 div.classList.add("list-group-item");
246 this.dropdown_count++;
247 const label = document.createElement("H5")
248 label.appendChild(document.createTextNode(node['name']))
249 div.appendChild(label);
250 div.appendChild(this.make_input(div, node, prepopulate));
251 div.appendChild(this.make_remove_button(div, node));
252 document.getElementById("dropdown_wrapper").appendChild(div);
256 remove_dropdown(div_id, node_id){
257 const div = document.getElementById(div_id);
258 const node = this.filter_items[node_id]
259 const parent = div.parentNode;
260 div.parentNode.removeChild(div);
261 delete this.result[node.class][node.id]['values'][div.id];
263 //checks if we have removed last item in class
264 if(jQuery.isEmptyObject(this.result[node.class][node.id]['values'])){
265 delete this.result[node.class][node.id];
271 if(!node['multiple']){
272 this.result[node.class][node.id] = {selected: node.selected, id: node.model_id}
274 delete this.result[node.class][node.id];
278 updateObjectResult(node, childKey, childValue){
279 if(!this.result[node.class][node.id])
280 this.result[node.class][node.id] = {selected: true, id: node.model_id, values: {}}
282 this.result[node.class][node.id]['values'][childKey] = childValue;
286 document.getElementById("filter_field").value = JSON.stringify(this.result);
291 constructor(debug, xml, hosts, added_hosts, removed_host_ids, graphContainer, overviewContainer, toolbarContainer){
292 if(!this.check_support())
295 this.currentWindow = null;
297 this.netColors = ['red', 'blue', 'purple', 'green', 'orange', '#8CCDF5', '#1E9BAC'];
299 this.lastHostBottom = 100;
300 this.networks = new Set();
301 this.has_public_net = false;
303 this.editor = new mxEditor();
304 this.graph = this.editor.graph;
306 this.editor.setGraphContainer(graphContainer);
307 this.doGlobalConfig();
308 this.prefill(xml, hosts, added_hosts, removed_host_ids);
309 this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', '', "/static/img/mxgraph/zoom_in.png", true);
310 this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', '', "/static/img/mxgraph/zoom_out.png", true);
313 this.editor.addAction('printXML', function(editor, cell) {
314 mxLog.write(this.encodeGraph());
317 this.addToolbarButton(this.editor, toolbarContainer, 'printXML', '', '/static/img/mxgraph/fit_to_size.png', true);
320 new mxOutline(this.graph, overviewContainer);
321 //sets the edge color to be the same as the network
322 this.graph.addListener(mxEvent.CELL_CONNECTED, function(sender, event) {this.cellConnectionHandler(sender, event)}.bind(this));
323 //hooks up double click functionality
324 this.graph.dblClick = function(evt, cell) {this.doubleClickHandler(evt, cell);}.bind(this);
326 if(!this.has_public_net){
327 this.addPublicNetwork();
332 if (!mxClient.isBrowserSupported()) {
333 mxUtils.error('Browser is not supported', 200, false);
339 prefill(xml, hosts, added_hosts, removed_host_ids){
340 //populate existing data
342 this.restoreFromXml(xml, this.editor);
344 for(const host of hosts)
350 for(const host of added_hosts)
352 this.updateHosts([]); //TODO: why?
354 this.updateHosts(removed_host_ids);
357 cellConnectionHandler(sender, event){
358 const edge = event.getProperty('edge');
359 const terminal = event.getProperty('terminal')
360 const source = event.getProperty('source');
361 if(this.checkAllowed(edge, terminal, source)) {
362 this.colorEdge(edge, terminal, source);
363 this.alertVlan(edge, terminal, source);
367 doubleClickHandler(evt, cell) {
369 if( cell.getParent() != null && cell.getParent().getId().indexOf("network") > -1) {
370 cell = cell.getParent();
372 if( cell.isEdge() || cell.getId().indexOf("network") > -1 ) {
373 this.createDeleteDialog(cell.getId());
376 this.showDetailWindow(cell);
381 alertVlan(edge, terminal, source) {
382 if( terminal == null || edge.getTerminal(!source) == null) {
385 const form = document.createElement("form");
386 const tagged = document.createElement("input");
387 tagged.type = "radio";
388 tagged.name = "tagged";
389 tagged.value = "True";
390 form.appendChild(tagged);
391 form.appendChild(document.createTextNode(" Tagged"));
392 form.appendChild(document.createElement("br"));
394 const untagged = document.createElement("input");
395 untagged.type = "radio";
396 untagged.name = "tagged";
397 untagged.value = "False";
398 form.appendChild(untagged);
399 form.appendChild(document.createTextNode(" Untagged"));
400 form.appendChild(document.createElement("br"));
402 const yes_button = document.createElement("button");
403 yes_button.onclick = function() {this.parseVlanWindow(edge.getId());}.bind(this);
404 yes_button.appendChild(document.createTextNode("Okay"));
406 const cancel_button = document.createElement("button");
407 cancel_button.onclick = function() {this.deleteVlanWindow(edge.getId());}.bind(this);
408 cancel_button.appendChild(document.createTextNode("Cancel"));
410 const error_div = document.createElement("div");
411 error_div.id = "current_window_errors";
412 form.appendChild(error_div);
414 const content = document.createElement('div');
415 content.appendChild(form);
416 content.appendChild(yes_button);
417 content.appendChild(cancel_button);
418 this.showWindow("Vlan Selection", content, 200, 200);
421 createDeleteDialog(id) {
422 const content = document.createElement('div');
423 const remove_button = document.createElement("button");
424 remove_button.style.width = '46%';
425 remove_button.onclick = function() { this.deleteCell(id);}.bind(this);
426 remove_button.appendChild(document.createTextNode("Remove"));
427 const cancel_button = document.createElement("button");
428 cancel_button.style.width = '46%';
429 cancel_button.onclick = function() { this.closeWindow();}.bind(this);
430 cancel_button.appendChild(document.createTextNode("Cancel"));
432 content.appendChild(remove_button);
433 content.appendChild(cancel_button);
434 this.showWindow('Do you want to delete this network?', content, 200, 62);
437 checkAllowed(edge, terminal, source) {
438 //check if other terminal is null, and that they are different
439 const otherTerminal = edge.getTerminal(!source);
440 if(terminal != null && otherTerminal != null) {
441 if( terminal.getParent().getId().split('_')[0] == //'host' or 'network'
442 otherTerminal.getParent().getId().split('_')[0] ) {
444 this.graph.removeCells([edge]);
451 colorEdge(edge, terminal, source) {
452 if(terminal.getParent().getId().indexOf('network') >= 0) {
453 const styles = terminal.getParent().getStyle().split(';');
455 for(let style of styles){
456 const kvp = style.split('=');
457 if(kvp[0] == "fillColor"){
461 edge.setStyle('strokeColor=' + color);
465 showDetailWindow(cell) {
466 const info = JSON.parse(cell.getValue());
467 const content = document.createElement("div");
468 const pre_tag = document.createElement("pre");
469 pre_tag.appendChild(document.createTextNode("Name: " + info.name + "\nDescription:\n" + info.description));
470 const ok_button = document.createElement("button");
471 ok_button.onclick = function() { this.closeWindow();};
472 content.appendChild(pre_tag);
473 content.appendChild(ok_button);
474 this.showWindow('Details', content, 400, 400);
477 restoreFromXml(xml, editor) {
478 const doc = mxUtils.parseXml(xml);
479 const node = doc.documentElement;
480 editor.readGraphModel(node);
482 //Iterate over all children, and parse the networks to add them to the sidebar
483 for( const cell of this.graph.getModel().getChildren(this.graph.getDefaultParent())) {
484 if(cell.getId().indexOf("network") > -1) {
485 const info = JSON.parse(cell.getValue());
486 const name = info['name'];
487 this.networks.add(name);
488 const styles = cell.getStyle().split(";");
490 for(const style of styles){
491 const kvp = style.split('=');
492 if(kvp[0] == "fillColor") {
498 this.has_public_net = true;
501 this.makeSidebarNetwork(name, color, cell.getId());
507 var cell = this.graph.getModel().getCell(cellId);
508 if( cellId.indexOf("network") > -1 ) {
509 let elem = document.getElementById(cellId);
510 elem.parentElement.removeChild(elem);
512 this.graph.removeCells([cell]);
513 this.currentWindow.destroy();
517 const input = document.createElement("input");
519 input.name = "net_name";
520 input.maxlength = 100;
521 input.id = "net_name_input";
522 input.style.margin = "5px";
524 const yes_button = document.createElement("button");
525 yes_button.onclick = function() {this.parseNetworkWindow();}.bind(this);
526 yes_button.appendChild(document.createTextNode("Okay"));
528 const cancel_button = document.createElement("button");
529 cancel_button.onclick = function() {this.closeWindow();}.bind(this);
530 cancel_button.appendChild(document.createTextNode("Cancel"));
532 const error_div = document.createElement("div");
533 error_div.id = "current_window_errors";
535 const content = document.createElement("div");
536 content.appendChild(document.createTextNode("Name: "));
537 content.appendChild(input);
538 content.appendChild(document.createElement("br"));
539 content.appendChild(yes_button);
540 content.appendChild(cancel_button);
541 content.appendChild(document.createElement("br"));
542 content.appendChild(error_div);
544 this.showWindow("Network Creation", content, 300, 300);
547 parseNetworkWindow() {
548 const net_name = document.getElementById("net_name_input").value
549 const error_div = document.getElementById("current_window_errors");
550 if( this.networks.has(net_name) ){
551 error_div.innerHTML = "All network names must be unique";
554 this.addNetwork(net_name);
555 this.currentWindow.destroy();
558 addToolbarButton(editor, toolbar, action, label, image, isTransparent) {
559 const button = document.createElement('button');
560 button.style.fontSize = '10';
562 const img = document.createElement('img');
563 img.setAttribute('src', image);
564 img.style.width = '16px';
565 img.style.height = '16px';
566 img.style.verticalAlign = 'middle';
567 img.style.marginRight = '2px';
568 button.appendChild(img);
571 button.style.background = 'transparent';
572 button.style.color = '#FFFFFF';
573 button.style.border = 'none';
575 mxEvent.addListener(button, 'click', function(evt) {
576 editor.execute(action);
578 mxUtils.write(button, label);
579 toolbar.appendChild(button);
583 const encoder = new mxCodec();
584 const xml = encoder.encode(this.graph.getModel());
585 return mxUtils.getXml(xml);
589 //general graph stuff
590 this.graph.setMultigraph(false);
591 this.graph.setCellsSelectable(false);
592 this.graph.setCellsMovable(false);
595 this.graph.vertexLabelIsMovable = true;
598 this.graph.setConnectable(true);
599 this.graph.setAllowDanglingEdges(false);
600 mxEdgeHandler.prototype.snapToTerminals = true;
601 mxConstants.MIN_HOTSPOT_SIZE = 16;
602 mxConstants.DEFAULT_HOTSPOT = 1;
603 //edge 'style' (still affects behavior greatly)
604 const style = this.graph.getStylesheet().getDefaultEdgeStyle();
605 style[mxConstants.STYLE_EDGE] = mxConstants.EDGESTYLE_ELBOW;
606 style[mxConstants.STYLE_ENDARROW] = mxConstants.NONE;
607 style[mxConstants.STYLE_ROUNDED] = true;
608 style[mxConstants.STYLE_FONTCOLOR] = 'black';
609 style[mxConstants.STYLE_STROKECOLOR] = 'red';
610 style[mxConstants.STYLE_LABEL_BACKGROUNDCOLOR] = '#FFFFFF';
611 style[mxConstants.STYLE_STROKEWIDTH] = '3';
612 style[mxConstants.STYLE_ROUNDED] = true;
613 style[mxConstants.STYLE_EDGE] = mxEdgeStyle.EntityRelation;
615 const hostStyle = this.graph.getStylesheet().getDefaultVertexStyle();
616 hostStyle[mxConstants.STYLE_ROUNDED] = 1;
618 this.graph.convertValueToString = function(cell) {
620 //changes value for edges with xml value
622 if(JSON.parse(cell.getValue())["tagged"]) {
628 return JSON.parse(cell.getValue())['name'];
632 return cell.getValue();
637 showWindow(title, content, width, height) {
638 //create transparent black background
639 const background = document.createElement('div');
640 background.style.position = 'absolute';
641 background.style.left = '0px';
642 background.style.top = '0px';
643 background.style.right = '0px';
644 background.style.bottom = '0px';
645 background.style.background = 'black';
646 mxUtils.setOpacity(background, 50);
647 document.body.appendChild(background);
649 const x = Math.max(0, document.body.scrollWidth/2-width/2);
650 const y = Math.max(10, (document.body.scrollHeight ||
651 document.documentElement.scrollHeight)/2-height*2/3);
653 const wnd = new mxWindow(title, content, x, y, width, height, false, true);
654 wnd.setClosable(false);
656 wnd.addListener(mxEvent.DESTROY, function(evt) {
657 this.graph.setEnabled(true);
658 mxEffects.fadeOut(background, 50, true, 10, 30, true);
660 this.currentWindow = wnd;
662 this.graph.setEnabled(false);
663 this.currentWindow.setVisible(true);
667 //allows the current window to be destroyed
668 this.currentWindow.destroy();
671 othersUntagged(edgeID) {
672 const edge = this.graph.getModel().getCell(edgeID);
673 const end1 = edge.getTerminal(true);
674 const end2 = edge.getTerminal(false);
676 if( end1.getParent().getId().split('_')[0] == 'host' ){
682 var edges = netint.edges;
683 for( let edge of edges) {
684 if( edge.getValue() ) {
685 var tagged = JSON.parse(edge.getValue()).tagged;
697 deleteVlanWindow(edgeID) {
698 const cell = this.graph.getModel().getCell(edgeID);
699 this.graph.removeCells([cell]);
700 this.currentWindow.destroy();
703 parseVlanWindow(edgeID) {
704 //do parsing and data manipulation
705 const radios = document.getElementsByName("tagged");
706 const edge = this.graph.getModel().getCell(edgeID);
708 for(let radio of radios){
710 //set edge to be tagged or untagged
711 if( radio.value == "False") {
712 if( this.othersUntagged(edgeID) ) {
713 document.getElementById("current_window_errors").innerHTML = "Only one untagged vlan per interface is allowed.";
717 const edgeVal = {tagged: radio.value == "True"};
718 edge.setValue(JSON.stringify(edgeVal));
722 this.graph.refresh(edge);
726 makeMxNetwork(net_name, is_public = false) {
727 const model = this.graph.getModel();
730 const xoff = 400 + (30 * this.netCount);
732 let color = this.netColors[this.netCount];
733 if( this.netCount > (this.netColors.length - 1)) {
734 color = Math.floor(Math.random() * 16777215); //int in possible color space
735 color = '#' + color.toString(16).toUpperCase(); //convert to hex
737 const net_val = { name: net_name, public: is_public};
738 const net = this.graph.insertVertex(
739 this.graph.getDefaultParent(),
740 'network_' + this.netCount,
741 JSON.stringify(net_val),
746 'fillColor=' + color,
749 const num_ports = 45;
750 for(var i=0; i<num_ports; i++){
751 let port = this.graph.insertVertex(
759 'fillColor=black;opacity=0',
764 const ret_val = { color: color, element_id: "network_" + this.netCount };
766 this.networks.add(net_name);
772 const net = this.makeMxNetwork("public", true);
773 this.makeSidebarNetwork("public", net['color'], net['element_id']);
774 this.has_public_net = true;
777 addNetwork(net_name) {
778 const ret = this.makeMxNetwork(net_name);
779 this.makeSidebarNetwork(net_name, ret.color, ret.element_id);
782 updateHosts(removed) {
784 for(const hostID of removed) {
785 cells.push(this.graph.getModel().getCell("host_" + hostID));
787 this.graph.removeCells(cells);
789 const hosts = this.graph.getChildVertices(this.graph.getDefaultParent());
791 for(const i in hosts) {
792 const host = hosts[i];
793 if(host.id.startsWith("host_")){
794 const geometry = host.getGeometry();
795 geometry.y = topdist + 50;
796 topdist = geometry.y + geometry.height;
797 host.setGeometry(geometry);
802 makeSidebarNetwork(net_name, color, net_id){
803 const colorBlob = document.createElement("div");
804 colorBlob.className = "square-20 rounded-circle";
805 colorBlob.style['background'] = color;
807 const textContainer = document.createElement("span");
808 textContainer.className = "ml-2";
809 textContainer.appendChild(document.createTextNode(net_name));
811 const timesIcon = document.createElement("i");
812 timesIcon.classList.add("fas", "fa-times");
814 const deletebutton = document.createElement("button");
815 deletebutton.className = "btn btn-danger ml-auto square-20 p-0 d-flex justify-content-center";
816 deletebutton.appendChild(timesIcon);
817 deletebutton.addEventListener("click", function() { this.createDeleteDialog(net_id); }.bind(this), false);
819 const newNet = document.createElement("li");
820 newNet.classList.add("list-group-item", "d-flex", "bg-light");
822 newNet.appendChild(colorBlob);
823 newNet.appendChild(textContainer);
825 if( net_name != "public" ) {
826 newNet.appendChild(deletebutton);
828 document.getElementById("network_list").appendChild(newNet);
832 const value = JSON.stringify(hostInfo['value']);
833 const interfaces = hostInfo['interfaces'];
835 const height = (25 * interfaces.length) + 25;
837 const yoff = this.lastHostBottom + 50;
838 this.lastHostBottom = yoff + height;
839 const host = this.graph.insertVertex(
840 this.graph.getDefaultParent(),
841 'host_' + hostInfo['id'],
850 host.getGeometry().offset = new mxPoint(-50,0);
851 host.setConnectable(false);
854 for(var i=0; i<interfaces.length; i++) {
855 const port = this.graph.insertVertex(
858 JSON.stringify(interfaces[i]),
863 'fillColor=blue;editable=0',
866 port.getGeometry().offset = new mxPoint(-4*interfaces[i].name.length -2,0);
867 this.graph.refresh(port);
869 this.graph.refresh(host);
873 const input_elem = document.getElementById("hidden_xml_input");
874 input_elem.value = this.encodeGraph(this.graph);
878 class SearchableSelectMultipleWidget {
879 constructor(format_vars, field_dataset, field_initial) {
880 this.format_vars = format_vars;
881 this.items = field_dataset;
882 this.initial = field_initial;
884 this.expanded_name_trie = {"isComplete": false};
885 this.small_name_trie = {"isComplete": false};
886 this.string_trie = {"isComplete": false};
888 this.added_items = new Set();
890 for( let e of ["show_from_noentry", "show_x_results", "results_scrollable", "selectable_limit", "placeholder"] )
892 this[e] = format_vars[e];
895 this.search_field_init();
897 if( this.show_from_noentry )
904 const textfield = document.getElementById("user_field");
905 const drop = document.getElementById("drop_results");
907 textfield.disabled = "True";
908 drop.style.display = "none";
910 const btns = document.getElementsByClassName("btn-remove");
911 for( const btn of btns )
913 btn.classList.add("disabled");
918 search_field_init() {
919 this.build_all_tries(this.items);
921 for( const elem of this.initial )
923 this.select_item(elem);
925 if(this.initial.length == 1)
927 this.search(this.items[this.initial[0]]["small_name"]);
928 document.getElementById("user_field").value = this.items[this.initial[0]]["small_name"];
932 build_all_tries(dict)
934 for( const key in dict )
936 this.add_item(dict[key]);
942 const id = item['id'];
943 this.add_to_tree(item['expanded_name'], id, this.expanded_name_trie);
944 this.add_to_tree(item['small_name'], id, this.small_name_trie);
945 this.add_to_tree(item['string'], id, this.string_trie);
948 add_to_tree(str, id, trie)
950 let inner_trie = trie;
953 if( !inner_trie[str.charAt(0)] )
956 inner_trie[str.charAt(0)] = new_trie;
960 var new_trie = inner_trie[str.charAt(0)];
963 if( str.length == 1 )
965 new_trie.isComplete = true;
970 new_trie.ids.push(id);
972 inner_trie = new_trie;
973 str = str.substring(1);
979 if( input.length == 0 && !this.show_from_noentry){
983 else if( input.length == 0 && this.show_from_noentry)
985 this.dropdown(this.items); //show all items
990 const tr1 = this.getSubtree(input, this.expanded_name_trie);
992 const tr2 = this.getSubtree(input, this.small_name_trie);
994 const tr3 = this.getSubtree(input, this.string_trie);
996 const results = this.collate(trees);
997 this.dropdown(results);
1001 getSubtree(input, given_trie)
1004 recursive function to return the trie accessed at input
1007 if( input.length == 0 ){
1012 const substr = input.substring(0, input.length - 1);
1013 const last_char = input.charAt(input.length-1);
1014 const subtrie = this.getSubtree(substr, given_trie);
1016 if( !subtrie ) //substr not in the trie
1021 const indexed_trie = subtrie[last_char];
1022 return indexed_trie;
1029 takes in a trie and returns a list of its item id's
1034 return itemIDs; //empty, base case
1036 for( const key in trie )
1042 itemIDs = itemIDs.concat(this.serialize(trie[key]));
1044 if ( trie.isComplete )
1046 itemIDs.push(...trie.ids);
1055 takes a list of tries
1056 returns a list of ids of objects that are available
1059 for( const tree of trees )
1061 const available_IDs = this.serialize(tree);
1063 for( const itemID of available_IDs ) {
1064 results[itemID] = this.items[itemID];
1070 generate_element_text(obj)
1072 const content_strings = [obj.expanded_name, obj.small_name, obj.string].filter(x => Boolean(x));
1073 const result = content_strings.shift();
1074 if( result == null || content_strings.length < 1) {
1077 return result + " (" + content_strings.join(", ") + ")";
1084 takes in a mapping of ids to objects in items
1085 and displays them in the dropdown
1087 const drop = document.getElementById("drop_results");
1088 while(drop.firstChild)
1090 drop.removeChild(drop.firstChild);
1093 for( const id in ids )
1095 const obj = this.items[id];
1096 const result_text = this.generate_element_text(obj);
1097 const result_entry = document.createElement("a");
1098 result_entry.href = "#";
1099 result_entry.innerText = result_text;
1100 result_entry.title = result_text;
1101 result_entry.classList.add("list-group-item", "list-group-item-action", "overflow-ellipsis", "flex-shrink-0");
1102 result_entry.onclick = function() { searchable_select_multiple_widget.select_item(obj.id); };
1103 const tooltip = document.createElement("span");
1104 const tooltiptext = document.createTextNode(result_text);
1105 tooltip.appendChild(tooltiptext);
1106 tooltip.classList.add("d-none");
1107 result_entry.appendChild(tooltip);
1108 drop.appendChild(result_entry);
1111 const scroll_restrictor = document.getElementById("scroll_restrictor");
1113 if( !drop.firstChild )
1115 scroll_restrictor.style.visibility = 'hidden';
1119 scroll_restrictor.style.visibility = 'inherit';
1123 select_item(item_id)
1125 if( (this.selectable_limit > -1 && this.added_items.size < this.selectable_limit) || this.selectable_limit < 0 )
1127 this.added_items.add(item_id);
1129 this.update_selected_list();
1130 // clear search bar contents
1131 document.getElementById("user_field").value = "";
1132 document.getElementById("user_field").focus();
1136 remove_item(item_id)
1138 this.added_items.delete(item_id);
1140 this.update_selected_list()
1141 document.getElementById("user_field").focus();
1144 update_selected_list()
1146 document.getElementById("added_number").innerText = this.added_items.size;
1147 const selector = document.getElementById('selector');
1148 selector.value = JSON.stringify([...this.added_items]);
1149 const added_list = document.getElementById('added_list');
1151 while(selector.firstChild)
1153 selector.removeChild(selector.firstChild);
1155 while(added_list.firstChild)
1157 added_list.removeChild(added_list.firstChild);
1160 const list_html = document.createElement("div");
1161 list_html.classList.add("list-group");
1163 for( const item_id of this.added_items )
1165 const times = document.createElement("li");
1166 times.classList.add("fas", "fa-times");
1168 const deleteButton = document.createElement("a");
1169 deleteButton.href = "#";
1170 deleteButton.innerHTML = "<i class='fas fa-times'></i>"
1171 // Setting .onclick/.addEventListener does not work,
1172 // which is why I took the setAttribute approach
1173 // If anyone knows why, please let me know :]
1174 deleteButton.setAttribute("onclick", `searchable_select_multiple_widget.remove_item(${item_id});`);
1175 deleteButton.classList.add("btn");
1176 const deleteColumn = document.createElement("div");
1177 deleteColumn.classList.add("col-auto");
1178 deleteColumn.append(deleteButton);
1180 const item = this.items[item_id];
1181 const element_entry_text = this.generate_element_text(item);
1182 const textColumn = document.createElement("div");
1183 textColumn.classList.add("col", "overflow-ellipsis");
1184 textColumn.innerText = element_entry_text;
1185 textColumn.title = element_entry_text;
1187 const itemRow = document.createElement("div");
1188 itemRow.classList.add("list-group-item", "d-flex", "p-0", "align-items-center");
1189 itemRow.append(textColumn, deleteColumn);
1191 list_html.append(itemRow);
1193 added_list.innerHTML = list_html.innerHTML;