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('disabled_node', 'cleared_node');
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('cleared_node')
110 elem.classList.remove('disabled_node', 'selected_node');
114 const elem = document.getElementById(node['id']);
115 node['selected'] = false;
116 node['selectable'] = false;
117 elem.classList.remove('cleared_node', 'selected_node');
118 elem.classList.add('disabled_node');
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");
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.pattern = "(?=^.{1,253}$)(^([A-Za-z0-9-_]{1,62}\.)*[A-Za-z0-9-_]{1,63})";
187 input.title = "Only alphanumeric characters (a-z, A-Z, 0-9), underscore(_), and hyphen (-) are allowed"
188 input.placeholder = node.form.placeholder;
189 this.inputs.push(input);
191 input.onchange = function() { that.updateObjectResult(node, div.id, input.value); that.restrictchars(this); };
192 input.oninput = function() { that.restrictchars(this); };
194 input.value = prepopulate;
198 add_item_prepopulate(node, prepopulate){
199 const div = document.createElement("DIV");
200 div.id = "dropdown_" + this.dropdown_count;
201 div.classList.add("dropdown_item");
202 this.dropdown_count++;
203 const label = document.createElement("H5")
204 label.appendChild(document.createTextNode(node['name']))
205 div.appendChild(label);
206 div.appendChild(this.make_input(div, node, prepopulate));
207 div.appendChild(this.make_remove_button(div, node));
208 document.getElementById("dropdown_wrapper").appendChild(div);
212 remove_dropdown(div_id, node_id){
213 const div = document.getElementById(div_id);
214 const node = this.filter_items[node_id]
215 const parent = div.parentNode;
216 div.parentNode.removeChild(div);
217 delete this.result[node.class][node.id]['values'][div.id];
219 //checks if we have removed last item in class
220 if(jQuery.isEmptyObject(this.result[node.class][node.id]['values'])){
221 delete this.result[node.class][node.id];
227 if(!node['multiple']){
228 this.result[node.class][node.id] = {selected: node.selected, id: node.model_id}
230 delete this.result[node.class][node.id];
234 updateObjectResult(node, childKey, childValue){
235 if(!this.result[node.class][node.id])
236 this.result[node.class][node.id] = {selected: true, id: node.model_id, values: {}}
238 this.result[node.class][node.id]['values'][childKey] = childValue;
242 document.getElementById("filter_field").value = JSON.stringify(this.result);
247 constructor(debug, xml, hosts, added_hosts, removed_host_ids, graphContainer, overviewContainer, toolbarContainer){
248 if(!this.check_support())
251 this.currentWindow = null;
253 this.netColors = ['red', 'blue', 'purple', 'green', 'orange', '#8CCDF5', '#1E9BAC'];
255 this.lastHostBottom = 100;
256 this.networks = new Set();
257 this.has_public_net = false;
259 this.editor = new mxEditor();
260 this.graph = this.editor.graph;
262 this.editor.setGraphContainer(graphContainer);
263 this.doGlobalConfig();
264 this.prefill(xml, hosts, added_hosts, removed_host_ids);
265 this.addToolbarButton(this.editor, toolbarContainer, 'zoomIn', '', "/static/img/mxgraph/zoom_in.png", true);
266 this.addToolbarButton(this.editor, toolbarContainer, 'zoomOut', '', "/static/img/mxgraph/zoom_out.png", true);
269 this.editor.addAction('printXML', function(editor, cell) {
270 mxLog.write(this.encodeGraph());
273 this.addToolbarButton(this.editor, toolbarContainer, 'printXML', '', '/static/img/mxgraph/fit_to_size.png', true);
276 new mxOutline(this.graph, overviewContainer);
277 //sets the edge color to be the same as the network
278 this.graph.addListener(mxEvent.CELL_CONNECTED, function(sender, event) {this.cellConnectionHandler(sender, event)}.bind(this));
279 //hooks up double click functionality
280 this.graph.dblClick = function(evt, cell) {this.doubleClickHandler(evt, cell);}.bind(this);
282 if(!this.has_public_net){
283 this.addPublicNetwork();
288 if (!mxClient.isBrowserSupported()) {
289 mxUtils.error('Browser is not supported', 200, false);
295 prefill(xml, hosts, added_hosts, removed_host_ids){
296 //populate existing data
298 this.restoreFromXml(xml, this.editor);
300 for(const host of hosts)
306 for(const host of added_hosts)
308 this.updateHosts([]); //TODO: why?
310 this.updateHosts(removed_host_ids);
313 cellConnectionHandler(sender, event){
314 const edge = event.getProperty('edge');
315 const terminal = event.getProperty('terminal')
316 const source = event.getProperty('source');
317 if(this.checkAllowed(edge, terminal, source)) {
318 this.colorEdge(edge, terminal, source);
319 this.alertVlan(edge, terminal, source);
323 doubleClickHandler(evt, cell) {
325 if( cell.getParent() != null && cell.getParent().getId().indexOf("network") > -1) {
326 cell = cell.getParent();
328 if( cell.isEdge() || cell.getId().indexOf("network") > -1 ) {
329 this.createDeleteDialog(cell.getId());
332 this.showDetailWindow(cell);
337 alertVlan(edge, terminal, source) {
338 if( terminal == null || edge.getTerminal(!source) == null) {
341 const form = document.createElement("form");
342 const tagged = document.createElement("input");
343 tagged.type = "radio";
344 tagged.name = "tagged";
345 tagged.value = "True";
346 form.appendChild(tagged);
347 form.appendChild(document.createTextNode(" Tagged"));
348 form.appendChild(document.createElement("br"));
350 const untagged = document.createElement("input");
351 untagged.type = "radio";
352 untagged.name = "tagged";
353 untagged.value = "False";
354 form.appendChild(untagged);
355 form.appendChild(document.createTextNode(" Untagged"));
356 form.appendChild(document.createElement("br"));
358 const yes_button = document.createElement("button");
359 yes_button.onclick = function() {this.parseVlanWindow(edge.getId());}.bind(this);
360 yes_button.appendChild(document.createTextNode("Okay"));
362 const cancel_button = document.createElement("button");
363 cancel_button.onclick = function() {this.deleteVlanWindow(edge.getId());}.bind(this);
364 cancel_button.appendChild(document.createTextNode("Cancel"));
366 const error_div = document.createElement("div");
367 error_div.id = "current_window_errors";
368 form.appendChild(error_div);
370 const content = document.createElement('div');
371 content.appendChild(form);
372 content.appendChild(yes_button);
373 content.appendChild(cancel_button);
374 this.showWindow("Vlan Selection", content, 200, 200);
377 createDeleteDialog(id) {
378 const content = document.createElement('div');
379 const remove_button = document.createElement("button");
380 remove_button.style.width = '46%';
381 remove_button.onclick = function() { this.deleteCell(id);}.bind(this);
382 remove_button.appendChild(document.createTextNode("Remove"));
383 const cancel_button = document.createElement("button");
384 cancel_button.style.width = '46%';
385 cancel_button.onclick = function() { this.closeWindow();}.bind(this);
386 cancel_button.appendChild(document.createTextNode("Cancel"));
388 content.appendChild(remove_button);
389 content.appendChild(cancel_button);
390 this.showWindow('Do you want to delete this network?', content, 200, 62);
393 checkAllowed(edge, terminal, source) {
394 //check if other terminal is null, and that they are different
395 const otherTerminal = edge.getTerminal(!source);
396 if(terminal != null && otherTerminal != null) {
397 if( terminal.getParent().getId().split('_')[0] == //'host' or 'network'
398 otherTerminal.getParent().getId().split('_')[0] ) {
400 this.graph.removeCells([edge]);
407 colorEdge(edge, terminal, source) {
408 if(terminal.getParent().getId().indexOf('network') >= 0) {
409 const styles = terminal.getParent().getStyle().split(';');
411 for(let style of styles){
412 const kvp = style.split('=');
413 if(kvp[0] == "fillColor"){
417 edge.setStyle('strokeColor=' + color);
421 showDetailWindow(cell) {
422 const info = JSON.parse(cell.getValue());
423 const content = document.createElement("div");
424 const pre_tag = document.createElement("pre");
425 pre_tag.appendChild(document.createTextNode("Name: " + info.name + "\nDescription:\n" + info.description));
426 const ok_button = document.createElement("button");
427 ok_button.onclick = function() { this.closeWindow();};
428 content.appendChild(pre_tag);
429 content.appendChild(ok_button);
430 this.showWindow('Details', content, 400, 400);
433 restoreFromXml(xml, editor) {
434 const doc = mxUtils.parseXml(xml);
435 const node = doc.documentElement;
436 editor.readGraphModel(node);
438 //Iterate over all children, and parse the networks to add them to the sidebar
439 for( const cell of this.graph.getModel().getChildren(this.graph.getDefaultParent())) {
440 if(cell.getId().indexOf("network") > -1) {
441 const info = JSON.parse(cell.getValue());
442 const name = info['name'];
443 this.networks.add(name);
444 const styles = cell.getStyle().split(";");
446 for(const style of styles){
447 const kvp = style.split('=');
448 if(kvp[0] == "fillColor") {
454 this.has_public_net = true;
457 this.makeSidebarNetwork(name, color, cell.getId());
463 var cell = this.graph.getModel().getCell(cellId);
464 if( cellId.indexOf("network") > -1 ) {
465 let elem = document.getElementById(cellId);
466 elem.parentElement.removeChild(elem);
468 this.graph.removeCells([cell]);
469 this.currentWindow.destroy();
473 const input = document.createElement("input");
475 input.name = "net_name";
476 input.maxlength = 100;
477 input.id = "net_name_input";
478 input.style.margin = "5px";
480 const yes_button = document.createElement("button");
481 yes_button.onclick = function() {this.parseNetworkWindow();}.bind(this);
482 yes_button.appendChild(document.createTextNode("Okay"));
484 const cancel_button = document.createElement("button");
485 cancel_button.onclick = function() {thid.closeWindow();}.bind(this);
486 cancel_button.appendChild(document.createTextNode("Cancel"));
488 const error_div = document.createElement("div");
489 error_div.id = "current_window_errors";
491 const content = document.createElement("div");
492 content.appendChild(document.createTextNode("Name: "));
493 content.appendChild(input);
494 content.appendChild(document.createElement("br"));
495 content.appendChild(yes_button);
496 content.appendChild(cancel_button);
497 content.appendChild(document.createElement("br"));
498 content.appendChild(error_div);
500 this.showWindow("Network Creation", content, 300, 300);
503 parseNetworkWindow() {
504 const net_name = document.getElementById("net_name_input").value
505 const error_div = document.getElementById("current_window_errors");
506 if( this.networks.has(net_name) ){
507 error_div.innerHTML = "All network names must be unique";
510 this.addNetwork(net_name);
511 this.currentWindow.destroy();
514 addToolbarButton(editor, toolbar, action, label, image, isTransparent) {
515 const button = document.createElement('button');
516 button.style.fontSize = '10';
518 const img = document.createElement('img');
519 img.setAttribute('src', image);
520 img.style.width = '16px';
521 img.style.height = '16px';
522 img.style.verticalAlign = 'middle';
523 img.style.marginRight = '2px';
524 button.appendChild(img);
527 button.style.background = 'transparent';
528 button.style.color = '#FFFFFF';
529 button.style.border = 'none';
531 mxEvent.addListener(button, 'click', function(evt) {
532 editor.execute(action);
534 mxUtils.write(button, label);
535 toolbar.appendChild(button);
539 const encoder = new mxCodec();
540 const xml = encoder.encode(this.graph.getModel());
541 return mxUtils.getXml(xml);
545 //general graph stuff
546 this.graph.setMultigraph(false);
547 this.graph.setCellsSelectable(false);
548 this.graph.setCellsMovable(false);
551 this.graph.vertexLabelIsMovable = true;
554 this.graph.setConnectable(true);
555 this.graph.setAllowDanglingEdges(false);
556 mxEdgeHandler.prototype.snapToTerminals = true;
557 mxConstants.MIN_HOTSPOT_SIZE = 16;
558 mxConstants.DEFAULT_HOTSPOT = 1;
559 //edge 'style' (still affects behavior greatly)
560 const style = this.graph.getStylesheet().getDefaultEdgeStyle();
561 style[mxConstants.STYLE_EDGE] = mxConstants.EDGESTYLE_ELBOW;
562 style[mxConstants.STYLE_ENDARROW] = mxConstants.NONE;
563 style[mxConstants.STYLE_ROUNDED] = true;
564 style[mxConstants.STYLE_FONTCOLOR] = 'black';
565 style[mxConstants.STYLE_STROKECOLOR] = 'red';
566 style[mxConstants.STYLE_LABEL_BACKGROUNDCOLOR] = '#FFFFFF';
567 style[mxConstants.STYLE_STROKEWIDTH] = '3';
568 style[mxConstants.STYLE_ROUNDED] = true;
569 style[mxConstants.STYLE_EDGE] = mxEdgeStyle.EntityRelation;
571 const hostStyle = this.graph.getStylesheet().getDefaultVertexStyle();
572 hostStyle[mxConstants.STYLE_ROUNDED] = 1;
574 this.graph.convertValueToString = function(cell) {
576 //changes value for edges with xml value
578 if(JSON.parse(cell.getValue())["tagged"]) {
584 return JSON.parse(cell.getValue())['name'];
588 return cell.getValue();
593 showWindow(title, content, width, height) {
594 //create transparent black background
595 const background = document.createElement('div');
596 background.style.position = 'absolute';
597 background.style.left = '0px';
598 background.style.top = '0px';
599 background.style.right = '0px';
600 background.style.bottom = '0px';
601 background.style.background = 'black';
602 mxUtils.setOpacity(background, 50);
603 document.body.appendChild(background);
605 const x = Math.max(0, document.body.scrollWidth/2-width/2);
606 const y = Math.max(10, (document.body.scrollHeight ||
607 document.documentElement.scrollHeight)/2-height*2/3);
609 const wnd = new mxWindow(title, content, x, y, width, height, false, true);
610 wnd.setClosable(false);
612 wnd.addListener(mxEvent.DESTROY, function(evt) {
613 this.graph.setEnabled(true);
614 mxEffects.fadeOut(background, 50, true, 10, 30, true);
616 this.currentWindow = wnd;
618 this.graph.setEnabled(false);
619 this.currentWindow.setVisible(true);
623 //allows the current window to be destroyed
624 this.currentWindow.destroy();
627 othersUntagged(edgeID) {
628 const edge = this.graph.getModel().getCell(edgeID);
629 const end1 = edge.getTerminal(true);
630 const end2 = edge.getTerminal(false);
632 if( end1.getParent().getId().split('_')[0] == 'host' ){
638 var edges = netint.edges;
639 for( let edge of edges) {
640 if( edge.getValue() ) {
641 var tagged = JSON.parse(edge.getValue()).tagged;
653 deleteVlanWindow(edgeID) {
654 const cell = this.graph.getModel().getCell(edgeID);
655 this.graph.removeCells([cell]);
656 this.currentWindow.destroy();
659 parseVlanWindow(edgeID) {
660 //do parsing and data manipulation
661 const radios = document.getElementsByName("tagged");
662 const edge = this.graph.getModel().getCell(edgeID);
664 for(let radio of radios){
666 //set edge to be tagged or untagged
667 if( radio.value == "False") {
668 if( this.othersUntagged(edgeID) ) {
669 document.getElementById("current_window_errors").innerHTML = "Only one untagged vlan per interface is allowed.";
673 const edgeVal = {tagged: radio.value == "True"};
674 edge.setValue(JSON.stringify(edgeVal));
678 this.graph.refresh(edge);
682 makeMxNetwork(net_name, is_public = false) {
683 const model = this.graph.getModel();
686 const xoff = 400 + (30 * this.netCount);
688 let color = this.netColors[this.netCount];
689 if( this.netCount > (this.netColors.length - 1)) {
690 color = Math.floor(Math.random() * 16777215); //int in possible color space
691 color = '#' + color.toString(16).toUpperCase(); //convert to hex
693 const net_val = { name: net_name, public: is_public};
694 const net = this.graph.insertVertex(
695 this.graph.getDefaultParent(),
696 'network_' + this.netCount,
697 JSON.stringify(net_val),
702 'fillColor=' + color,
705 const num_ports = 45;
706 for(var i=0; i<num_ports; i++){
707 let port = this.graph.insertVertex(
715 'fillColor=black;opacity=0',
720 const ret_val = { color: color, element_id: "network_" + this.netCount };
722 this.networks.add(net_name);
728 const net = this.makeMxNetwork("public", true);
729 this.makeSidebarNetwork("public", net['color'], net['element_id']);
730 this.has_public_net = true;
733 addNetwork(net_name) {
734 const ret = this.makeMxNetwork(net_name);
735 this.makeSidebarNetwork(net_name, ret.color, ret.element_id);
738 updateHosts(removed) {
740 for(const hostID of removed) {
741 cells.push(this.graph.getModel().getCell("host_" + hostID));
743 this.graph.removeCells(cells);
745 const hosts = this.graph.getChildVertices(this.graph.getDefaultParent());
747 for(const i in hosts) {
748 const host = hosts[i];
749 if(host.id.startsWith("host_")){
750 const geometry = host.getGeometry();
751 geometry.y = topdist + 50;
752 topdist = geometry.y + geometry.height;
753 host.setGeometry(geometry);
758 makeSidebarNetwork(net_name, color, net_id){
759 const newNet = document.createElement("li");
760 const colorBlob = document.createElement("div");
761 colorBlob.className = "colorblob";
762 const textContainer = document.createElement("p");
763 textContainer.className = "network_innertext";
765 const deletebutton = document.createElement("button");
766 deletebutton.className = "btn btn-danger";
767 deletebutton.style = "float: right; height: 20px; line-height: 8px; vertical-align: middle; width: 20px; padding-left: 5px;";
768 deletebutton.appendChild(document.createTextNode("X"));
769 deletebutton.addEventListener("click", function() { this.createDeleteDialog(net_id); }.bind(this), false);
770 textContainer.appendChild(document.createTextNode(net_name));
771 colorBlob.style['background'] = color;
772 newNet.appendChild(colorBlob);
773 newNet.appendChild(textContainer);
774 if( net_name != "public" ) {
775 newNet.appendChild(deletebutton);
777 document.getElementById("network_list").appendChild(newNet);
781 const value = JSON.stringify(hostInfo['value']);
782 const interfaces = hostInfo['interfaces'];
784 const height = (25 * interfaces.length) + 25;
786 const yoff = this.lastHostBottom + 50;
787 this.lastHostBottom = yoff + height;
788 const host = this.graph.insertVertex(
789 this.graph.getDefaultParent(),
790 'host_' + hostInfo['id'],
799 host.getGeometry().offset = new mxPoint(-50,0);
800 host.setConnectable(false);
803 for(var i=0; i<interfaces.length; i++) {
804 const port = this.graph.insertVertex(
807 JSON.stringify(interfaces[i]),
812 'fillColor=blue;editable=0',
815 port.getGeometry().offset = new mxPoint(-4*interfaces[i].name.length -2,0);
816 this.graph.refresh(port);
818 this.graph.refresh(host);
822 const form = document.getElementById("xml_form");
823 const input_elem = document.getElementById("hidden_xml_input");
824 input_elem.value = this.encodeGraph(this.graph);
825 const req = new XMLHttpRequest();
826 req.open("POST", "/wf/workflow/", false);
827 req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
828 req.onerror = function() { alert("problem with form submission"); }
829 const formData = $("#xml_form").serialize();
834 class SearchableSelectMultipleWidget {
835 constructor(format_vars, field_dataset, field_initial) {
836 this.format_vars = format_vars;
837 this.items = field_dataset;
838 this.initial = field_initial;
840 this.expanded_name_trie = {"isComplete": false};
841 this.small_name_trie = {"isComplete": false};
842 this.string_trie = {"isComplete": false};
844 this.added_items = new Set();
846 for( let e of ["show_from_noentry", "show_x_results", "results_scrollable", "selectable_limit", "placeholder"] )
848 this[e] = format_vars[e];
851 this.search_field_init();
853 if( this.show_from_noentry )
860 const textfield = document.getElementById("user_field");
861 const drop = document.getElementById("drop_results");
863 textfield.disabled = "True";
864 drop.style.display = "none";
866 const btns = document.getElementsByClassName("btn-remove");
867 for( const btn of btns )
869 btn.classList.add("disabled");
874 search_field_init() {
875 this.build_all_tries(this.items);
877 for( const elem of this.initial )
879 this.select_item(elem);
881 if(this.initial.length == 1)
883 this.search(this.items[this.initial[0]]["small_name"]);
884 document.getElementById("user_field").value = this.items[this.initial[0]]["small_name"];
888 build_all_tries(dict)
890 for( const key in dict )
892 this.add_item(dict[key]);
898 const id = item['id'];
899 this.add_to_tree(item['expanded_name'], id, this.expanded_name_trie);
900 this.add_to_tree(item['small_name'], id, this.small_name_trie);
901 this.add_to_tree(item['string'], id, this.string_trie);
904 add_to_tree(str, id, trie)
906 let inner_trie = trie;
909 if( !inner_trie[str.charAt(0)] )
912 inner_trie[str.charAt(0)] = new_trie;
916 var new_trie = inner_trie[str.charAt(0)];
919 if( str.length == 1 )
921 new_trie.isComplete = true;
926 new_trie.ids.push(id);
928 inner_trie = new_trie;
929 str = str.substring(1);
935 if( input.length == 0 && !this.show_from_noentry){
939 else if( input.length == 0 && this.show_from_noentry)
941 this.dropdown(this.items); //show all items
946 const tr1 = this.getSubtree(input, this.expanded_name_trie);
948 const tr2 = this.getSubtree(input, this.small_name_trie);
950 const tr3 = this.getSubtree(input, this.string_trie);
952 const results = this.collate(trees);
953 this.dropdown(results);
957 getSubtree(input, given_trie)
960 recursive function to return the trie accessed at input
963 if( input.length == 0 ){
968 const substr = input.substring(0, input.length - 1);
969 const last_char = input.charAt(input.length-1);
970 const subtrie = this.getSubtree(substr, given_trie);
972 if( !subtrie ) //substr not in the trie
977 const indexed_trie = subtrie[last_char];
985 takes in a trie and returns a list of its item id's
990 return itemIDs; //empty, base case
992 for( const key in trie )
998 itemIDs = itemIDs.concat(this.serialize(trie[key]));
1000 if ( trie.isComplete )
1002 itemIDs.push(...trie.ids);
1011 takes a list of tries
1012 returns a list of ids of objects that are available
1015 for( const tree of trees )
1017 const available_IDs = this.serialize(tree);
1019 for( const itemID of available_IDs ) {
1020 results[itemID] = this.items[itemID];
1026 generate_element_text(obj)
1028 const content_strings = [obj.expanded_name, obj.small_name, obj.string].filter(x => Boolean(x));
1029 const result = content_strings.shift();
1030 if( result == null || content_strings.length < 1) {
1033 return result + " (" + content_strings.join(", ") + ")";
1040 takes in a mapping of ids to objects in items
1041 and displays them in the dropdown
1043 const drop = document.getElementById("drop_results");
1044 while(drop.firstChild)
1046 drop.removeChild(drop.firstChild);
1049 for( const id in ids )
1051 const result_entry = document.createElement("li");
1052 const result_button = document.createElement("a");
1053 const obj = this.items[id];
1054 const result_text = this.generate_element_text(obj);
1055 result_button.appendChild(document.createTextNode(result_text));
1056 result_button.onclick = function() { searchable_select_multiple_widget.select_item(obj.id); };
1057 const tooltip = document.createElement("span");
1058 const tooltiptext = document.createTextNode(result_text);
1059 tooltip.appendChild(tooltiptext);
1060 tooltip.setAttribute('class', 'entry_tooltip');
1061 result_button.appendChild(tooltip);
1062 result_entry.appendChild(result_button);
1063 drop.appendChild(result_entry);
1066 const scroll_restrictor = document.getElementById("scroll_restrictor");
1068 if( !drop.firstChild )
1070 scroll_restrictor.style.visibility = 'hidden';
1074 scroll_restrictor.style.visibility = 'inherit';
1078 select_item(item_id)
1080 if( (this.selectable_limit > -1 && this.added_items.size < this.selectable_limit) || this.selectable_limit < 0 )
1082 this.added_items.add(item_id);
1084 this.update_selected_list();
1085 // clear search bar contents
1086 document.getElementById("user_field").value = "";
1087 document.getElementById("user_field").focus();
1091 remove_item(item_id)
1093 this.added_items.delete(item_id);
1095 this.update_selected_list()
1096 document.getElementById("user_field").focus();
1099 update_selected_list()
1101 document.getElementById("added_number").innerText = this.added_items.size;
1102 const selector = document.getElementById('selector');
1103 selector.value = JSON.stringify([...this.added_items]);
1104 const added_list = document.getElementById('added_list');
1106 while(selector.firstChild)
1108 selector.removeChild(selector.firstChild);
1110 while(added_list.firstChild)
1112 added_list.removeChild(added_list.firstChild);
1117 for( const item_id of this.added_items )
1119 const item = this.items[item_id];
1121 const element_entry_text = this.generate_element_text(item);
1123 list_html += '<div class="list_entry">'
1124 + '<p class="added_entry_text">'
1125 + element_entry_text
1127 + '<button onclick="searchable_select_multiple_widget.remove_item('
1129 + ')" class="btn-remove btn">remove</button>';
1130 list_html += '</div>';
1132 added_list.innerHTML = list_html;