<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script> <div class="autocomplete"> <div id="warning_pane" style="background: #FFFFFF; color: #CC0000;"> {% if incompatible == "true" %} <h3>Warning: Incompatible Configuration</h3> <p>Please make a different selection, as the current config conflicts with the selected pod</p> {% endif %} </div> <input id="user_field" name="ignore_this" class="form-control" autocomplete="off" type="text" placeholder="{{placeholder}}" value="" oninput="search(this.value)" {% if disabled %} disabled {% endif %} > </input> <input type="hidden" id="selector" name="{{ name }}" class="form-control" style="display: none;" {% if disabled %} disabled {% endif %} > </input> <ul id="drop_results"></ul> <div id="added_list"> </div> <div id="added_counter"> <p id="added_number">0</p> <p id="addable_limit">/ {% if selectable_limit > -1 %} {{ selectable_limit }} {% else %} ∞ {% endif %}added</p> </div> <style> #user_field { font-size: 14pt; padding: 5px; } #drop_results{ list-style-type: none; padding: 0; margin: 0; max-height: 300px; min-height: 0; overflow-y: scroll; overflow-x: hidden; border: solid 1px #ddd; border-top: none; border-bottom: none; display: none; } #drop_results li a{ font-size: 14pt; background-color: #f6f6f6; padding: 7px; text-decoration: none; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #drop_results li a { border-bottom: 1px solid #ddd; } .list_entry { border: 1px solid #ccc; border-radius: 5px; margin-top: 5px; vertical-align: middle; line-height: 40px; height: 40px; padding-left: 12px; width: 100%; display: flex; } #drop_results li a:hover{ background-color: #ffffff; } .added_entry_text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: inline; width: 100%; } .btn-remove { float: right; height: 30px; margin: 4px; padding: 1px; max-width: 20%; width: 15%; min-width: 70px; overflow: hidden; text-overflow: ellipsis; } .entry_tooltip { display: none; } #drop_results li a:hover .entry_tooltip { display: block; position: absolute; background: #444; color: #ddd; text-align: center; font-size: 12pt; border-radius: 3px; } #drop_results { max-width: 100%; display: inline-block; list-style-type: none; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } #drop_results li { overflow: hidden; text-overflow: ellipsis; } #added_counter { text-align: center; } #added_number, #addable_limit { display: inline; } </style> </div> <script type="text/javascript"> //flags var show_from_noentry = {{show_from_noentry|yesno:"true,false"}}; // whether to show any results before user starts typing var show_x_results = {{show_x_results|default:-1}}; // how many results to show at a time, -1 shows all results var results_scrollable = {{results_scrollable|yesno:"true,false"}}; // whether list should be scrollable var selectable_limit = {{selectable_limit|default:-1}}; // how many selections can be made, -1 allows infinitely many var placeholder = "{{placeholder|default:"begin typing"}}"; // placeholder that goes in text box //needed info var items = {{items|safe}} // items to add to trie. Type is a dictionary of dictionaries with structure: /* { id# : { "id": any, identifiable on backend "small_name": string, displayed first (before separator), searchable (use for e.g. username) "expanded_name": string, displayed second (after separator), searchable (use for e.g. email address) "string": string, not displayed, still searchable } } */ /* used later: {{ selectable_limit }}: changes what number displays for field {{ name }}: form identifiable name, relevant for backend // when submitted, form will contain field data in post with name as the key {{ placeholder }}: "greyed out" contents put into search field initially to guide user as to what they're searching for {{ initial }}: in search_field_init(), marked safe, an array of id's each referring to an id from items */ //tries var expanded_name_trie = {} expanded_name_trie.isComplete = false; var small_name_trie = {} small_name_trie.isComplete = false; var string_trie = {} string_trie.isComplete = false; var added_items = []; search_field_init(); if( show_from_noentry ) { search(""); } function disable() { var textfield = document.getElementById("user_field"); var drop = document.getElementById("drop_results"); textfield.disabled = "True"; drop.style.display = "none"; var btns = document.getElementsByClassName("btn-remove"); for( var i = 0; i < btns.length; i++ ) { btns[i].classList.add("disabled"); } } function search_field_init() { build_all_tries(items); var initial = {{ initial|safe }}; for( var i = 0; i < initial.length; i++) { select_item(String(initial[i])); } if(initial.length == 1) { search(items[initial[0]]["small_name"]); document.getElementById("user_field").value = items[initial[0]]["small_name"]; } } function build_all_tries(dict) { for( var i in dict ) { add_item(dict[i]); } } function add_item(item) { var id = item['id']; add_to_tree(item['expanded_name'], id, expanded_name_trie); add_to_tree(item['small_name'], id, small_name_trie); add_to_tree(item['string'], id, string_trie); } function add_to_tree(str, id, trie) { inner_trie = trie; while( str ) { if( !inner_trie[str.charAt(0)] ) { new_trie = {}; inner_trie[str.charAt(0)] = new_trie; } else { new_trie = inner_trie[str.charAt(0)]; } if( str.length == 1 ) { new_trie.isComplete = true; new_trie.itemID = id; } inner_trie = new_trie; str = str.substring(1); } } function search(input) { if( input.length == 0 && !show_from_noentry){ dropdown([]); return; } else if( input.length == 0 && show_from_noentry) { dropdown(items); //show all items } else { var trees = [] var tr1 = getSubtree(input, expanded_name_trie); trees.push(tr1); var tr2 = getSubtree(input, small_name_trie); trees.push(tr2); var tr3 = getSubtree(input, string_trie); trees.push(tr3); var results = collate(trees); dropdown(results); } } function getSubtree(input, given_trie) { /* recursive function to return the trie accessed at input */ if( input.length == 0 ){ return given_trie; } else{ var substr = input.substring(0, input.length - 1); var last_char = input.charAt(input.length-1); var subtrie = getSubtree(substr, given_trie); if( !subtrie ) //substr not in the trie { return {}; } var indexed_trie = subtrie[last_char]; return indexed_trie; } } function serialize(trie) { /* takes in a trie and returns a list of its item id's */ var itemIDs = []; if ( !trie ) { return itemIDs; //empty, base case } for( var key in trie ) { if(key.length > 1) { continue; } itemIDs = itemIDs.concat(serialize(trie[key])); } if ( trie.isComplete ) { itemIDs.push( trie.itemID ); } return itemIDs; } function collate(trees) { /* takes a list of tries returns a list of ids of objects that are available */ results = []; for( var i in trees ) { var available_IDs = serialize(trees[i]); for( var j=0; j<available_IDs.length; j++){ var itemID = available_IDs[j]; results[itemID] = items[itemID]; } } return results; } function generate_element_text(obj) { var content_strings = [obj['expanded_name'], obj['small_name'], obj['string']].filter(x => Boolean(x)); var result = content_strings.shift(); if( result == null || content_strings.length < 1) return result; return result + " (" + content_strings.join(", ") + ")"; } function dropdown(ids) { /* takes in a mapping of ids to objects in items and displays them in the dropdown */ var drop = document.getElementById("drop_results"); while(drop.firstChild) { drop.removeChild(drop.firstChild); } for( var id in ids ) { var result_entry = document.createElement("li"); var result_button = document.createElement("a"); var obj = items[id]; var result_text = generate_element_text(obj); result_button.appendChild(document.createTextNode(result_text)); result_button.setAttribute('onclick', 'select_item("' + obj['id'] + '")'); var tooltip = document.createElement("span"); var tooltiptext = document.createTextNode(result_text); tooltip.appendChild(tooltiptext); tooltip.setAttribute('class', 'entry_tooltip'); result_button.appendChild(tooltip); result_entry.appendChild(result_button); drop.appendChild(result_entry); } if( !drop.firstChild ) { drop.style.display = 'none'; } else { drop.style.display = 'block'; } } function select_item(item_id) { //TODO make faster var item = items[item_id]['id']; if( (selectable_limit > -1 && added_items.length < selectable_limit) || selectable_limit < 0 ) { if( added_items.indexOf(item) == -1 ) { added_items.push(item); } } update_selected_list(); document.getElementById("user_field").focus(); } function remove_item(item_ref) { item = Object.values(items)[item_ref]; var index = added_items.indexOf(item); added_items.splice(index, 1); update_selected_list() document.getElementById("user_field").focus(); } function update_selected_list() { document.getElementById("added_number").innerText = added_items.length; selector = document.getElementById('selector'); selector.value = JSON.stringify(added_items); added_list = document.getElementById('added_list'); while(selector.firstChild) { selector.removeChild(selector.firstChild); } while(added_list.firstChild) { added_list.removeChild(added_list.firstChild); } list_html = ""; for( var key in added_items ) { item_id = added_items[key]; item = items[item_id]; var element_entry_text = generate_element_text(item); list_html += '<div class="list_entry">' + '<p class="added_entry_text">' + element_entry_text + '</p>' + '<button onclick="remove_item(' + Object.values(items).indexOf(item) + ')" class="btn-remove btn">remove</button>'; list_html += '</div>'; } added_list.innerHTML = list_html; } </script>