1 class MultipleSelectFilterWidget {
3 constructor(neighbors, items, initial) {
5 this.graph_neighbors = neighbors;
6 this.filter_items = items;
8 this.dropdown_count = 0;
10 for(let nodeId in this.filter_items) {
11 const node = this.filter_items[nodeId];
12 this.result[node.class] = {}
15 this.make_selection(initial);
18 make_selection( initial_data ){
19 if(!initial_data || jQuery.isEmptyObject(initial_data))
21 for(let item_class in initial_data) {
22 const selected_items = initial_data[item_class];
23 for( let node_id in selected_items ){
24 const node = this.filter_items[node_id];
25 const selection_data = selected_items[node_id]
26 if( selection_data.selected ) {
28 this.markAndSweep(node);
29 this.updateResult(node);
32 this.make_multiple_selection(node, selection_data);
38 make_multiple_selection(node, selection_data){
39 const prepop_data = selection_data.values;
40 for(let k in prepop_data){
41 const div = this.add_item_prepopulate(node, prepop_data[k]);
42 this.updateObjectResult(node, div.id, prepop_data[k]);
47 for(let i in this.filter_items) {
48 const node = this.filter_items[i];
49 node['marked'] = true; //mark all nodes
52 const toCheck = [root];
53 while(toCheck.length > 0){
54 const node = toCheck.pop();
56 continue; //already visited, just continue
58 node['marked'] = false; //mark as visited
59 if(node['follow'] || node == root){ //add neighbors if we want to follow this node
60 const neighbors = this.graph_neighbors[node.id];
61 for(let neighId of neighbors) {
62 const neighbor = this.filter_items[neighId];
63 toCheck.push(neighbor);
68 //now remove all nodes still marked
69 for(let i in this.filter_items){
70 const node = this.filter_items[i];
72 this.disable_node(node);
78 if(node['selected']) {
79 this.markAndSweep(node);
81 else { //TODO: make this not dumb
83 //remember the currently selected, then reset everything and reselect one at a time
84 for(let nodeId in this.filter_items) {
85 node = this.filter_items[nodeId];
86 if(node['selected']) {
91 for(let node of selected) {
93 this.markAndSweep(node);
99 const elem = document.getElementById(node['id']);
100 node['selected'] = true;
101 elem.classList.remove('bg-white', 'not-allowed', 'bg-light');
102 elem.classList.add('selected_node');
106 const elem = document.getElementById(node['id']);
107 node['selected'] = false;
108 node['selectable'] = true;
109 elem.classList.add('bg-white')
110 elem.classList.remove('not-allowed', 'bg-light', 'selected_node');
114 const elem = document.getElementById(node['id']);
115 node['selected'] = false;
116 node['selectable'] = false;
117 elem.classList.remove('bg-white', 'selected_node');
118 elem.classList.add('not-allowed', 'bg-light');
122 const node = this.filter_items[id];
123 if(!node['selectable'])
126 if(node['multiple']){
127 return this.processClickMultiple(node);
129 return this.processClickSingle(node);
133 processClickSingle(node){
134 node['selected'] = !node['selected']; //toggle on click
135 if(node['selected']) {
141 this.updateResult(node);
144 processClickMultiple(node){
146 const div = this.add_item_prepopulate(node, false);
148 this.updateObjectResult(node, div.id, "");
151 restrictchars(input){
152 if( input.validity.patternMismatch ){
153 input.setCustomValidity("Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed");
154 input.reportValidity();
156 input.value = input.value.replace(/([^A-Za-z0-9-_.])+/g, "");
157 this.checkunique(input);
160 checkunique(tocheck){ //TODO: use set
161 const val = tocheck.value;
162 for( let input of this.inputs ){
163 if( input.value == val && input != tocheck){
164 tocheck.setCustomValidity("All hostnames must be unique");
165 tocheck.reportValidity();
169 tocheck.setCustomValidity("");
172 make_remove_button(div, node){
173 const button = document.createElement("BUTTON");
174 button.type = "button";
175 button.appendChild(document.createTextNode("Remove"));
176 button.classList.add("btn", "btn-danger", "d-inline-block");
178 button.onclick = function(){ that.remove_dropdown(div.id, node.id); }
182 make_input(div, node, prepopulate){
183 const input = document.createElement("INPUT");
184 input.type = node.form.type;
185 input.name = node.id + node.form.name
186 input.classList.add("form-control", "w-auto", "d-inline-block");
187 input.pattern = "(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})";
188 input.title = "Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed"
189 input.placeholder = node.form.placeholder;
190 this.inputs.push(input);
192 input.onchange = function() { that.updateObjectResult(node, div.id, input.value); that.restrictchars(this); };
193 input.oninput = function() { that.restrictchars(this); };
195 input.value = prepopulate;
199 add_item_prepopulate(node, prepopulate){
200 const div = document.createElement("DIV");
201 div.id = "dropdown_" + this.dropdown_count;
202 div.classList.add("list-group-item");
203 this.dropdown_count++;
204 const label = document.createElement("H5")
205 label.appendChild(document.createTextNode(node['name']))
206 div.appendChild(label);
207 div.appendChild(this.make_input(div, node, prepopulate));
208 div.appendChild(this.make_remove_button(div, node));
209 document.getElementById("dropdown_wrapper").appendChild(div);
213 remove_dropdown(div_id, node_id){
214 const div = document.getElementById(div_id);
215 const node = this.filter_items[node_id]
216 const parent = div.parentNode;
217 div.parentNode.removeChild(div);
218 delete this.result[node.class][node.id]['values'][div.id];
220 //checks if we have removed last item in class
221 if(jQuery.isEmptyObject(this.result[node.class][node.id]['values'])){
222 delete this.result[node.class][node.id];
228 if(!node['multiple']){
229 this.result[node.class][node.id] = {selected: node.selected, id: node.model_id}
231 delete this.result[node.class][node.id];
235 updateObjectResult(node, childKey, childValue){
236 if(!this.result[node.class][node.id])
237 this.result[node.class][node.id] = {selected: true, id: node.model_id, values: {}}
239 this.result[node.class][node.id]['values'][childKey] = childValue;
243 document.getElementById("filter_field").value = JSON.stringify(this.result);
248 constructor(debug, xml, hosts, added_hosts, removed_host_ids, graphContainer, overviewContainer, toolbarContainer){
249 if(!this.check_support())
252 this.currentWindow = null;
254 this.netColors = ['red', 'blue', 'purple', 'green', 'orange', '#8CCDF5', '#1E9BAC'];
256 this.lastHostBottom = 100;
257 this.networks = new Set();
258 this.has_public_net = false;
260 this.editor = new mxEditor();
261 this.graph = this.editor.graph;
263 this.editor.setGraphContainer(graphContainer);
264 this.doGlobalConfig();
265 this.prefill(xml, hosts, added_hosts, removed_host_ids);
266 this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', '', "/static/img/mxgraph/zoom_in.png", true);
267 this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', '', "/static/img/mxgraph/zoom_out.png", true);
270 this.editor.addAction('printXML', function(editor, cell) {
271 mxLog.write(this.encodeGraph());
274 this.addToolbarButton(this.editor, toolbarContainer, 'printXML', '', '/static/img/mxgraph/fit_to_size.png', true);
277 new mxOutline(this.graph, overviewContainer);
278 //sets the edge color to be the same as the network
279 this.graph.addListener(mxEvent.CELL_CONNECTED, function(sender, event) {this.cellConnectionHandler(sender, event)}.bind(this));
280 //hooks up double click functionality
281 this.graph.dblClick = function(evt, cell) {this.doubleClickHandler(evt, cell);}.bind(this);
283 if(!this.has_public_net){
284 this.addPublicNetwork();
289 if (!mxClient.isBrowserSupported()) {
290 mxUtils.error('Browser is not supported', 200, false);
296 prefill(xml, hosts, added_hosts, removed_host_ids){
297 //populate existing data
299 this.restoreFromXml(xml, this.editor);
301 for(const host of hosts)
307 for(const host of added_hosts)
309 this.updateHosts([]); //TODO: why?
311 this.updateHosts(removed_host_ids);
314 cellConnectionHandler(sender, event){
315 const edge = event.getProperty('edge');
316 const terminal = event.getProperty('terminal')
317 const source = event.getProperty('source');
318 if(this.checkAllowed(edge, terminal, source)) {
319 this.colorEdge(edge, terminal, source);
320 this.alertVlan(edge, terminal, source);
324 doubleClickHandler(evt, cell) {
326 if( cell.getParent() != null && cell.getParent().getId().indexOf("network") > -1) {
327 cell = cell.getParent();
329 if( cell.isEdge() || cell.getId().indexOf("network") > -1 ) {
330 this.createDeleteDialog(cell.getId());
333 this.showDetailWindow(cell);
338 alertVlan(edge, terminal, source) {
339 if( terminal == null || edge.getTerminal(!source) == null) {
342 const form = document.createElement("form");
343 const tagged = document.createElement("input");
344 tagged.type = "radio";
345 tagged.name = "tagged";
346 tagged.value = "True";
347 form.appendChild(tagged);
348 form.appendChild(document.createTextNode(" Tagged"));
349 form.appendChild(document.createElement("br"));
351 const untagged = document.createElement("input");
352 untagged.type = "radio";
353 untagged.name = "tagged";
354 untagged.value = "False";
355 form.appendChild(untagged);
356 form.appendChild(document.createTextNode(" Untagged"));
357 form.appendChild(document.createElement("br"));
359 const yes_button = document.createElement("button");
360 yes_button.onclick = function() {this.parseVlanWindow(edge.getId());}.bind(this);
361 yes_button.appendChild(document.createTextNode("Okay"));
363 const cancel_button = document.createElement("button");
364 cancel_button.onclick = function() {this.deleteVlanWindow(edge.getId());}.bind(this);
365 cancel_button.appendChild(document.createTextNode("Cancel"));
367 const error_div = document.createElement("div");
368 error_div.id = "current_window_errors";
369 form.appendChild(error_div);
371 const content = document.createElement('div');
372 content.appendChild(form);
373 content.appendChild(yes_button);
374 content.appendChild(cancel_button);
375 this.showWindow("Vlan Selection", content, 200, 200);
378 createDeleteDialog(id) {
379 const content = document.createElement('div');
380 const remove_button = document.createElement("button");
381 remove_button.style.width = '46%';
382 remove_button.onclick = function() { this.deleteCell(id);}.bind(this);
383 remove_button.appendChild(document.createTextNode("Remove"));
384 const cancel_button = document.createElement("button");
385 cancel_button.style.width = '46%';
386 cancel_button.onclick = function() { this.closeWindow();}.bind(this);
387 cancel_button.appendChild(document.createTextNode("Cancel"));
389 content.appendChild(remove_button);
390 content.appendChild(cancel_button);
391 this.showWindow('Do you want to delete this network?', content, 200, 62);
394 checkAllowed(edge, terminal, source) {
395 //check if other terminal is null, and that they are different
396 const otherTerminal = edge.getTerminal(!source);
397 if(terminal != null && otherTerminal != null) {
398 if( terminal.getParent().getId().split('_')[0] == //'host' or 'network'
399 otherTerminal.getParent().getId().split('_')[0] ) {
401 this.graph.removeCells([edge]);
408 colorEdge(edge, terminal, source) {
409 if(terminal.getParent().getId().indexOf('network') >= 0) {
410 const styles = terminal.getParent().getStyle().split(';');
412 for(let style of styles){
413 const kvp = style.split('=');
414 if(kvp[0] == "fillColor"){
418 edge.setStyle('strokeColor=' + color);
422 showDetailWindow(cell) {
423 const info = JSON.parse(cell.getValue());
424 const content = document.createElement("div");
425 const pre_tag = document.createElement("pre");
426 pre_tag.appendChild(document.createTextNode("Name: " + info.name + "\nDescription:\n" + info.description));
427 const ok_button = document.createElement("button");
428 ok_button.onclick = function() { this.closeWindow();};
429 content.appendChild(pre_tag);
430 content.appendChild(ok_button);
431 this.showWindow('Details', content, 400, 400);
434 restoreFromXml(xml, editor) {
435 const doc = mxUtils.parseXml(xml);
436 const node = doc.documentElement;
437 editor.readGraphModel(node);
439 //Iterate over all children, and parse the networks to add them to the sidebar
440 for( const cell of this.graph.getModel().getChildren(this.graph.getDefaultParent())) {
441 if(cell.getId().indexOf("network") > -1) {
442 const info = JSON.parse(cell.getValue());
443 const name = info['name'];
444 this.networks.add(name);
445 const styles = cell.getStyle().split(";");
447 for(const style of styles){
448 const kvp = style.split('=');
449 if(kvp[0] == "fillColor") {
455 this.has_public_net = true;
458 this.makeSidebarNetwork(name, color, cell.getId());
464 var cell = this.graph.getModel().getCell(cellId);
465 if( cellId.indexOf("network") > -1 ) {
466 let elem = document.getElementById(cellId);
467 elem.parentElement.removeChild(elem);
469 this.graph.removeCells([cell]);
470 this.currentWindow.destroy();
474 const input = document.createElement("input");
476 input.name = "net_name";
477 input.maxlength = 100;
478 input.id = "net_name_input";
479 input.style.margin = "5px";
481 const yes_button = document.createElement("button");
482 yes_button.onclick = function() {this.parseNetworkWindow();}.bind(this);
483 yes_button.appendChild(document.createTextNode("Okay"));
485 const cancel_button = document.createElement("button");
486 cancel_button.onclick = function() {this.closeWindow();}.bind(this);
487 cancel_button.appendChild(document.createTextNode("Cancel"));
489 const error_div = document.createElement("div");
490 error_div.id = "current_window_errors";
492 const content = document.createElement("div");
493 content.appendChild(document.createTextNode("Name: "));
494 content.appendChild(input);
495 content.appendChild(document.createElement("br"));
496 content.appendChild(yes_button);
497 content.appendChild(cancel_button);
498 content.appendChild(document.createElement("br"));
499 content.appendChild(error_div);
501 this.showWindow("Network Creation", content, 300, 300);
504 parseNetworkWindow() {
505 const net_name = document.getElementById("net_name_input").value
506 const error_div = document.getElementById("current_window_errors");
507 if( this.networks.has(net_name) ){
508 error_div.innerHTML = "All network names must be unique";
511 this.addNetwork(net_name);
512 this.currentWindow.destroy();
515 addToolbarButton(editor, toolbar, action, label, image, isTransparent) {
516 const button = document.createElement('button');
517 button.style.fontSize = '10';
519 const img = document.createElement('img');
520 img.setAttribute('src', image);
521 img.style.width = '16px';
522 img.style.height = '16px';
523 img.style.verticalAlign = 'middle';
524 img.style.marginRight = '2px';
525 button.appendChild(img);
528 button.style.background = 'transparent';
529 button.style.color = '#FFFFFF';
530 button.style.border = 'none';
532 mxEvent.addListener(button, 'click', function(evt) {
533 editor.execute(action);
535 mxUtils.write(button, label);
536 toolbar.appendChild(button);
540 const encoder = new mxCodec();
541 const xml = encoder.encode(this.graph.getModel());
542 return mxUtils.getXml(xml);
546 //general graph stuff
547 this.graph.setMultigraph(false);
548 this.graph.setCellsSelectable(false);
549 this.graph.setCellsMovable(false);
552 this.graph.vertexLabelIsMovable = true;
555 this.graph.setConnectable(true);
556 this.graph.setAllowDanglingEdges(false);
557 mxEdgeHandler.prototype.snapToTerminals = true;
558 mxConstants.MIN_HOTSPOT_SIZE = 16;
559 mxConstants.DEFAULT_HOTSPOT = 1;
560 //edge 'style' (still affects behavior greatly)
561 const style = this.graph.getStylesheet().getDefaultEdgeStyle();
562 style[mxConstants.STYLE_EDGE] = mxConstants.EDGESTYLE_ELBOW;
563 style[mxConstants.STYLE_ENDARROW] = mxConstants.NONE;
564 style[mxConstants.STYLE_ROUNDED] = true;
565 style[mxConstants.STYLE_FONTCOLOR] = 'black';
566 style[mxConstants.STYLE_STROKECOLOR] = 'red';
567 style[mxConstants.STYLE_LABEL_BACKGROUNDCOLOR] = '#FFFFFF';
568 style[mxConstants.STYLE_STROKEWIDTH] = '3';
569 style[mxConstants.STYLE_ROUNDED] = true;
570 style[mxConstants.STYLE_EDGE] = mxEdgeStyle.EntityRelation;
572 const hostStyle = this.graph.getStylesheet().getDefaultVertexStyle();
573 hostStyle[mxConstants.STYLE_ROUNDED] = 1;
575 this.graph.convertValueToString = function(cell) {
577 //changes value for edges with xml value
579 if(JSON.parse(cell.getValue())["tagged"]) {
585 return JSON.parse(cell.getValue())['name'];
589 return cell.getValue();
594 showWindow(title, content, width, height) {
595 //create transparent black background
596 const background = document.createElement('div');
597 background.style.position = 'absolute';
598 background.style.left = '0px';
599 background.style.top = '0px';
600 background.style.right = '0px';
601 background.style.bottom = '0px';
602 background.style.background = 'black';
603 mxUtils.setOpacity(background, 50);
604 document.body.appendChild(background);
606 const x = Math.max(0, document.body.scrollWidth/2-width/2);
607 const y = Math.max(10, (document.body.scrollHeight ||
608 document.documentElement.scrollHeight)/2-height*2/3);
610 const wnd = new mxWindow(title, content, x, y, width, height, false, true);
611 wnd.setClosable(false);
613 wnd.addListener(mxEvent.DESTROY, function(evt) {
614 this.graph.setEnabled(true);
615 mxEffects.fadeOut(background, 50, true, 10, 30, true);
617 this.currentWindow = wnd;
619 this.graph.setEnabled(false);
620 this.currentWindow.setVisible(true);
624 //allows the current window to be destroyed
625 this.currentWindow.destroy();
628 othersUntagged(edgeID) {
629 const edge = this.graph.getModel().getCell(edgeID);
630 const end1 = edge.getTerminal(true);
631 const end2 = edge.getTerminal(false);
633 if( end1.getParent().getId().split('_')[0] == 'host' ){
639 var edges = netint.edges;
640 for( let edge of edges) {
641 if( edge.getValue() ) {
642 var tagged = JSON.parse(edge.getValue()).tagged;
654 deleteVlanWindow(edgeID) {
655 const cell = this.graph.getModel().getCell(edgeID);
656 this.graph.removeCells([cell]);
657 this.currentWindow.destroy();
660 parseVlanWindow(edgeID) {
661 //do parsing and data manipulation
662 const radios = document.getElementsByName("tagged");
663 const edge = this.graph.getModel().getCell(edgeID);
665 for(let radio of radios){
667 //set edge to be tagged or untagged
668 if( radio.value == "False") {
669 if( this.othersUntagged(edgeID) ) {
670 document.getElementById("current_window_errors").innerHTML = "Only one untagged vlan per interface is allowed.";
674 const edgeVal = {tagged: radio.value == "True"};
675 edge.setValue(JSON.stringify(edgeVal));
679 this.graph.refresh(edge);
683 makeMxNetwork(net_name, is_public = false) {
684 const model = this.graph.getModel();
687 const xoff = 400 + (30 * this.netCount);
689 let color = this.netColors[this.netCount];
690 if( this.netCount > (this.netColors.length - 1)) {
691 color = Math.floor(Math.random() * 16777215); //int in possible color space
692 color = '#' + color.toString(16).toUpperCase(); //convert to hex
694 const net_val = { name: net_name, public: is_public};
695 const net = this.graph.insertVertex(
696 this.graph.getDefaultParent(),
697 'network_' + this.netCount,
698 JSON.stringify(net_val),
703 'fillColor=' + color,
706 const num_ports = 45;
707 for(var i=0; i<num_ports; i++){
708 let port = this.graph.insertVertex(
716 'fillColor=black;opacity=0',
721 const ret_val = { color: color, element_id: "network_" + this.netCount };
723 this.networks.add(net_name);
729 const net = this.makeMxNetwork("public", true);
730 this.makeSidebarNetwork("public", net['color'], net['element_id']);
731 this.has_public_net = true;
734 addNetwork(net_name) {
735 const ret = this.makeMxNetwork(net_name);
736 this.makeSidebarNetwork(net_name, ret.color, ret.element_id);
739 updateHosts(removed) {
741 for(const hostID of removed) {
742 cells.push(this.graph.getModel().getCell("host_" + hostID));
744 this.graph.removeCells(cells);
746 const hosts = this.graph.getChildVertices(this.graph.getDefaultParent());
748 for(const i in hosts) {
749 const host = hosts[i];
750 if(host.id.startsWith("host_")){
751 const geometry = host.getGeometry();
752 geometry.y = topdist + 50;
753 topdist = geometry.y + geometry.height;
754 host.setGeometry(geometry);
759 makeSidebarNetwork(net_name, color, net_id){
760 const colorBlob = document.createElement("div");
761 colorBlob.className = "square-20 rounded-circle";
762 colorBlob.style['background'] = color;
764 const textContainer = document.createElement("span");
765 textContainer.className = "ml-2";
766 textContainer.appendChild(document.createTextNode(net_name));
768 const timesIcon = document.createElement("i");
769 timesIcon.classList.add("fas", "fa-times");
771 const deletebutton = document.createElement("button");
772 deletebutton.className = "btn btn-danger ml-auto square-20 p-0 d-flex justify-content-center";
773 deletebutton.appendChild(timesIcon);
774 deletebutton.addEventListener("click", function() { this.createDeleteDialog(net_id); }.bind(this), false);
776 const newNet = document.createElement("li");
777 newNet.classList.add("list-group-item", "d-flex", "bg-light");
779 newNet.appendChild(colorBlob);
780 newNet.appendChild(textContainer);
782 if( net_name != "public" ) {
783 newNet.appendChild(deletebutton);
785 document.getElementById("network_list").appendChild(newNet);
789 const value = JSON.stringify(hostInfo['value']);
790 const interfaces = hostInfo['interfaces'];
792 const height = (25 * interfaces.length) + 25;
794 const yoff = this.lastHostBottom + 50;
795 this.lastHostBottom = yoff + height;
796 const host = this.graph.insertVertex(
797 this.graph.getDefaultParent(),
798 'host_' + hostInfo['id'],
807 host.getGeometry().offset = new mxPoint(-50,0);
808 host.setConnectable(false);
811 for(var i=0; i<interfaces.length; i++) {
812 const port = this.graph.insertVertex(
815 JSON.stringify(interfaces[i]),
820 'fillColor=blue;editable=0',
823 port.getGeometry().offset = new mxPoint(-4*interfaces[i].name.length -2,0);
824 this.graph.refresh(port);
826 this.graph.refresh(host);
830 const form = document.getElementById("xml_form");
831 const input_elem = document.getElementById("hidden_xml_input");
832 input_elem.value = this.encodeGraph(this.graph);
833 const req = new XMLHttpRequest();
834 req.open("POST", "/wf/workflow/", false);
835 req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
836 req.onerror = function() { alert("problem with form submission"); }
837 const formData = $("#xml_form").serialize();
842 class SearchableSelectMultipleWidget {
843 constructor(format_vars, field_dataset, field_initial) {
844 this.format_vars = format_vars;
845 this.items = field_dataset;
846 this.initial = field_initial;
848 this.expanded_name_trie = {"isComplete": false};
849 this.small_name_trie = {"isComplete": false};
850 this.string_trie = {"isComplete": false};
852 this.added_items = new Set();
854 for( let e of ["show_from_noentry", "show_x_results", "results_scrollable", "selectable_limit", "placeholder"] )
856 this[e] = format_vars[e];
859 this.search_field_init();
861 if( this.show_from_noentry )
868 const textfield = document.getElementById("user_field");
869 const drop = document.getElementById("drop_results");
871 textfield.disabled = "True";
872 drop.style.display = "none";
874 const btns = document.getElementsByClassName("btn-remove");
875 for( const btn of btns )
877 btn.classList.add("disabled");
882 search_field_init() {
883 this.build_all_tries(this.items);
885 for( const elem of this.initial )
887 this.select_item(elem);
889 if(this.initial.length == 1)
891 this.search(this.items[this.initial[0]]["small_name"]);
892 document.getElementById("user_field").value = this.items[this.initial[0]]["small_name"];
896 build_all_tries(dict)
898 for( const key in dict )
900 this.add_item(dict[key]);
906 const id = item['id'];
907 this.add_to_tree(item['expanded_name'], id, this.expanded_name_trie);
908 this.add_to_tree(item['small_name'], id, this.small_name_trie);
909 this.add_to_tree(item['string'], id, this.string_trie);
912 add_to_tree(str, id, trie)
914 let inner_trie = trie;
917 if( !inner_trie[str.charAt(0)] )
920 inner_trie[str.charAt(0)] = new_trie;
924 var new_trie = inner_trie[str.charAt(0)];
927 if( str.length == 1 )
929 new_trie.isComplete = true;
934 new_trie.ids.push(id);
936 inner_trie = new_trie;
937 str = str.substring(1);
943 if( input.length == 0 && !this.show_from_noentry){
947 else if( input.length == 0 && this.show_from_noentry)
949 this.dropdown(this.items); //show all items
954 const tr1 = this.getSubtree(input, this.expanded_name_trie);
956 const tr2 = this.getSubtree(input, this.small_name_trie);
958 const tr3 = this.getSubtree(input, this.string_trie);
960 const results = this.collate(trees);
961 this.dropdown(results);
965 getSubtree(input, given_trie)
968 recursive function to return the trie accessed at input
971 if( input.length == 0 ){
976 const substr = input.substring(0, input.length - 1);
977 const last_char = input.charAt(input.length-1);
978 const subtrie = this.getSubtree(substr, given_trie);
980 if( !subtrie ) //substr not in the trie
985 const indexed_trie = subtrie[last_char];
993 takes in a trie and returns a list of its item id's
998 return itemIDs; //empty, base case
1000 for( const key in trie )
1006 itemIDs = itemIDs.concat(this.serialize(trie[key]));
1008 if ( trie.isComplete )
1010 itemIDs.push(...trie.ids);
1019 takes a list of tries
1020 returns a list of ids of objects that are available
1023 for( const tree of trees )
1025 const available_IDs = this.serialize(tree);
1027 for( const itemID of available_IDs ) {
1028 results[itemID] = this.items[itemID];
1034 generate_element_text(obj)
1036 const content_strings = [obj.expanded_name, obj.small_name, obj.string].filter(x => Boolean(x));
1037 const result = content_strings.shift();
1038 if( result == null || content_strings.length < 1) {
1041 return result + " (" + content_strings.join(", ") + ")";
1048 takes in a mapping of ids to objects in items
1049 and displays them in the dropdown
1051 const drop = document.getElementById("drop_results");
1052 while(drop.firstChild)
1054 drop.removeChild(drop.firstChild);
1057 for( const id in ids )
1059 const result_entry = document.createElement("li");
1060 const result_button = document.createElement("a");
1061 const obj = this.items[id];
1062 const result_text = this.generate_element_text(obj);
1063 result_entry.classList.add("list-group-item", "list-group-item-action");
1064 result_entry.innerText = result_text;
1065 result_entry.onclick = function() { searchable_select_multiple_widget.select_item(obj.id); };
1066 const tooltip = document.createElement("span");
1067 const tooltiptext = document.createTextNode(result_text);
1068 tooltip.appendChild(tooltiptext);
1069 tooltip.classList.add("d-none");
1070 result_entry.appendChild(tooltip);
1071 drop.appendChild(result_entry);
1074 const scroll_restrictor = document.getElementById("scroll_restrictor");
1076 if( !drop.firstChild )
1078 scroll_restrictor.style.visibility = 'hidden';
1082 scroll_restrictor.style.visibility = 'inherit';
1086 select_item(item_id)
1088 if( (this.selectable_limit > -1 && this.added_items.size < this.selectable_limit) || this.selectable_limit < 0 )
1090 this.added_items.add(item_id);
1092 this.update_selected_list();
1093 // clear search bar contents
1094 document.getElementById("user_field").value = "";
1095 document.getElementById("user_field").focus();
1099 remove_item(item_id)
1101 this.added_items.delete(item_id);
1103 this.update_selected_list()
1104 document.getElementById("user_field").focus();
1107 update_selected_list()
1109 document.getElementById("added_number").innerText = this.added_items.size;
1110 const selector = document.getElementById('selector');
1111 selector.value = JSON.stringify([...this.added_items]);
1112 const added_list = document.getElementById('added_list');
1114 while(selector.firstChild)
1116 selector.removeChild(selector.firstChild);
1118 while(added_list.firstChild)
1120 added_list.removeChild(added_list.firstChild);
1125 for( const item_id of this.added_items )
1127 const item = this.items[item_id];
1129 const element_entry_text = this.generate_element_text(item);
1131 list_html += '<div class="border rounded mt-2 w-100 d-flex align-items-center pl-2">'
1132 + element_entry_text
1133 + '<button onclick="searchable_select_multiple_widget.remove_item('
1135 + ')" class="btn btn-danger ml-auto">Remove</button>';
1136 list_html += '</div>';
1138 added_list.innerHTML = list_html;