diff options
author | Sawyer Bergeron <sawyerbergeron@gmail.com> | 2019-06-24 14:30:20 -0400 |
---|---|---|
committer | Sawyer Bergeron <sawyerbergeron@gmail.com> | 2019-06-25 15:27:52 -0400 |
commit | f8b55ea3e44af8f09412361e33f4c604be9fb397 (patch) | |
tree | 6ec16de944cf4d78b486b42ad16bfd8f026480e0 | |
parent | b73b6f2a0b168b9ca1587b9c379a3620a27f4627 (diff) |
Refactor searchable widget
Change-Id: I0d342a3f31769fe71059d08653002454851b61cc
Signed-off-by: Sawyer Bergeron <sawyerbergeron@gmail.com>
-rw-r--r-- | src/static/js/dashboard.js | 302 | ||||
-rw-r--r-- | src/templates/dashboard/searchable_select_multiple.html | 350 |
2 files changed, 337 insertions, 315 deletions
diff --git a/src/static/js/dashboard.js b/src/static/js/dashboard.js index e51a219..0ed61e8 100644 --- a/src/static/js/dashboard.js +++ b/src/static/js/dashboard.js @@ -830,3 +830,305 @@ class NetworkStep { req.send(formData); } } + +class SearchableSelectMultipleWidget { + constructor(format_vars, field_dataset, field_initial) { + this.format_vars = format_vars; + this.items = field_dataset; + this.initial = field_initial; + + this.expanded_name_trie = {"isComplete": false}; + this.small_name_trie = {"isComplete": false}; + this.string_trie = {"isComplete": false}; + + this.added_items = new Set(); + + for( let e of ["show_from_noentry", "show_x_results", "results_scrollable", "selectable_limit", "placeholder"] ) + { + this[e] = format_vars[e]; + } + + this.search_field_init(); + + if( this.show_from_noentry ) + { + this.search(""); + } + } + + disable() { + const textfield = document.getElementById("user_field"); + const drop = document.getElementById("drop_results"); + + textfield.disabled = "True"; + drop.style.display = "none"; + + const btns = document.getElementsByClassName("btn-remove"); + for( const btn of btns ) + { + btn.classList.add("disabled"); + btn.onclick = ""; + } + } + + search_field_init() { + this.build_all_tries(this.items); + + for( const elem of this.initial ) + { + this.select_item(elem); + } + if(this.initial.length == 1) + { + this.search(this.items[this.initial[0]]["small_name"]); + document.getElementById("user_field").value = this.items[this.initial[0]]["small_name"]; + } + } + + build_all_tries(dict) + { + for( const key in dict ) + { + this.add_item(dict[key]); + } + } + + add_item(item) + { + const id = item['id']; + this.add_to_tree(item['expanded_name'], id, this.expanded_name_trie); + this.add_to_tree(item['small_name'], id, this.small_name_trie); + this.add_to_tree(item['string'], id, this.string_trie); + } + + add_to_tree(str, id, trie) + { + let inner_trie = trie; + while( str ) + { + if( !inner_trie[str.charAt(0)] ) + { + var new_trie = {}; + inner_trie[str.charAt(0)] = new_trie; + } + else + { + var new_trie = inner_trie[str.charAt(0)]; + } + + if( str.length == 1 ) + { + new_trie.isComplete = true; + if( !new_trie.ids ) + { + new_trie.ids = []; + } + new_trie.ids.push(id); + } + inner_trie = new_trie; + str = str.substring(1); + } + } + + search(input) + { + if( input.length == 0 && !this.show_from_noentry){ + this.dropdown([]); + return; + } + else if( input.length == 0 && this.show_from_noentry) + { + this.dropdown(this.items); //show all items + } + else + { + const trees = [] + const tr1 = this.getSubtree(input, this.expanded_name_trie); + trees.push(tr1); + const tr2 = this.getSubtree(input, this.small_name_trie); + trees.push(tr2); + const tr3 = this.getSubtree(input, this.string_trie); + trees.push(tr3); + const results = this.collate(trees); + this.dropdown(results); + } + } + + getSubtree(input, given_trie) + { + /* + recursive function to return the trie accessed at input + */ + + if( input.length == 0 ){ + return given_trie; + } + + else{ + const substr = input.substring(0, input.length - 1); + const last_char = input.charAt(input.length-1); + const subtrie = this.getSubtree(substr, given_trie); + + if( !subtrie ) //substr not in the trie + { + return {}; + } + + const indexed_trie = subtrie[last_char]; + return indexed_trie; + } + } + + serialize(trie) + { + /* + takes in a trie and returns a list of its item id's + */ + let itemIDs = []; + if ( !trie ) + { + return itemIDs; //empty, base case + } + for( const key in trie ) + { + if(key.length > 1) + { + continue; + } + itemIDs = itemIDs.concat(this.serialize(trie[key])); + } + if ( trie.isComplete ) + { + itemIDs.push(...trie.ids); + } + + return itemIDs; + } + + collate(trees) + { + /* + takes a list of tries + returns a list of ids of objects that are available + */ + const results = []; + for( const tree of trees ) + { + const available_IDs = this.serialize(tree); + + for( const itemID of available_IDs ) { + results[itemID] = this.items[itemID]; + } + } + return results; + } + + generate_element_text(obj) + { + const content_strings = [obj.expanded_name, obj.small_name, obj.string].filter(x => Boolean(x)); + const result = content_strings.shift(); + if( result == null || content_strings.length < 1) { + return result; + } else { + return result + " (" + content_strings.join(", ") + ")"; + } + } + + dropdown(ids) + { + /* + takes in a mapping of ids to objects in items + and displays them in the dropdown + */ + const drop = document.getElementById("drop_results"); + while(drop.firstChild) + { + drop.removeChild(drop.firstChild); + } + + for( const id in ids ) + { + const result_entry = document.createElement("li"); + const result_button = document.createElement("a"); + const obj = this.items[id]; + const result_text = this.generate_element_text(obj); + result_button.appendChild(document.createTextNode(result_text)); + result_button.onclick = function() { searchable_select_multiple_widget.select_item(obj.id); }; + const tooltip = document.createElement("span"); + const 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); + } + + const scroll_restrictor = document.getElementById("scroll_restrictor"); + + if( !drop.firstChild ) + { + scroll_restrictor.style.visibility = 'hidden'; + } + else + { + scroll_restrictor.style.visibility = 'inherit'; + } + } + + select_item(item_id) + { + if( (this.selectable_limit > -1 && this.added_items.size < this.selectable_limit) || this.selectable_limit < 0 ) + { + this.added_items.add(item_id); + } + this.update_selected_list(); + // clear search bar contents + document.getElementById("user_field").value = ""; + document.getElementById("user_field").focus(); + this.search(""); + } + + remove_item(item_id) + { + this.added_items.delete(item_id); + + this.update_selected_list() + document.getElementById("user_field").focus(); + } + + update_selected_list() + { + document.getElementById("added_number").innerText = this.added_items.size; + const selector = document.getElementById('selector'); + selector.value = JSON.stringify([...this.added_items]); + const added_list = document.getElementById('added_list'); + + while(selector.firstChild) + { + selector.removeChild(selector.firstChild); + } + while(added_list.firstChild) + { + added_list.removeChild(added_list.firstChild); + } + + let list_html = ""; + + for( const item_id of this.added_items ) + { + const item = this.items[item_id]; + + const element_entry_text = this.generate_element_text(item); + + list_html += '<div class="list_entry">' + + '<p class="added_entry_text">' + + element_entry_text + + '</p>' + + '<button onclick="searchable_select_multiple_widget.remove_item(' + + item_id + + ')" class="btn-remove btn">remove</button>'; + list_html += '</div>'; + } + added_list.innerHTML = list_html; + } +} diff --git a/src/templates/dashboard/searchable_select_multiple.html b/src/templates/dashboard/searchable_select_multiple.html index 91ed09c..8bcf890 100644 --- a/src/templates/dashboard/searchable_select_multiple.html +++ b/src/templates/dashboard/searchable_select_multiple.html @@ -1,4 +1,5 @@ <script src="https://code.jquery.com/jquery-2.2.4.min.js"></script> +<script src="/static/js/dashboard.js"></script> <div id="search_select_outer" class="autocomplete"> <div id="warning_pane" style="background: #FFFFFF; color: #CC0000;"> @@ -16,7 +17,7 @@ </div> - <input id="user_field" name="ignore_this" class="form-control" autocomplete="off" type="text" placeholder="{{placeholder}}" value="" oninput="search(this.value)" + <input id="user_field" name="ignore_this" class="form-control" autocomplete="off" type="text" placeholder="{{placeholder}}" value="" oninput="searchable_select_multiple_widget.search(this.value)" {% if disabled %} disabled {% endif %} > </input> @@ -161,329 +162,48 @@ </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 - */ + function searchableSelectMultipleWidgetEntry() { + let format_vars = { + "show_from_noentry": {{show_from_noentry|yesno:"true,false"}}, + "show_x_results": {{show_x_results|default:-1}}, + "results_scrollable": {{results_scrollable|yesno:"true,false"}}, + "selectable_limit": {{selectable_limit|default:-1}}, + "placeholder": "{{placeholder|default:"begin typing"}}" + }; - //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; + let field_dataset = {{items|safe}}; - var added_items = []; + let field_initial = {{ initial|safe }}; - search_field_init(); - - if( show_from_noentry ) - { - search(""); + //global + searchable_select_multiple_widget = new SearchableSelectMultipleWidget(format_vars, field_dataset, field_initial); } - function disable() { - var textfield = document.getElementById("user_field"); - var drop = document.getElementById("drop_results"); - - textfield.disabled = "True"; - drop.style.display = "none"; + searchableSelectMultipleWidgetEntry(); - 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; - if( !new_trie.ids ) - { - new_trie.ids = []; - } - new_trie.ids.push(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; - } - } + /* + var show_from_noentry = context(show_from_noentry|yesno:"true,false") // whether to show any results before user starts typing + var show_x_results = context(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 = "context(placeholder|default:"begin typing")" // placeholder that goes in text box - function serialize(trie) - { - /* - takes in a trie and returns a list of its item id's - */ - var itemIDs = []; - if ( !trie ) + needed info + var items = context(items|safe) // items to add to trie. Type is a dictionary of dictionaries with structure: { - 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.ids); - } - - 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); - } - - var scroll_restrictor = document.getElementById("scroll_restrictor"); - - if( !drop.firstChild ) - { - scroll_restrictor.style.visibility = 'hidden'; - } - else - { - scroll_restrictor.style.visibility = 'inherit'; - } - } - - 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); + 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 } } - update_selected_list(); - // clear search bar contents - document.getElementById("user_field").value = ""; - document.getElementById("user_field").focus(); - search(""); - } - - 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; - } + used later: + context(selectable_limit): changes what number displays for field + context(name): form identifiable name, relevant for backend + // when submitted, form will contain field data in post with name as the key + context(placeholder): "greyed out" contents put into search field initially to guide user as to what they're searching for + context(initial): in search_field_init(), marked safe, an array of id's each referring to an id from items + */ </script> |