summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSawyer Bergeron <sawyerbergeron@gmail.com>2019-06-24 14:30:20 -0400
committerSawyer Bergeron <sawyerbergeron@gmail.com>2019-06-25 15:27:52 -0400
commitf8b55ea3e44af8f09412361e33f4c604be9fb397 (patch)
tree6ec16de944cf4d78b486b42ad16bfd8f026480e0
parentb73b6f2a0b168b9ca1587b9c379a3620a27f4627 (diff)
Refactor searchable widget
Change-Id: I0d342a3f31769fe71059d08653002454851b61cc Signed-off-by: Sawyer Bergeron <sawyerbergeron@gmail.com>
-rw-r--r--src/static/js/dashboard.js302
-rw-r--r--src/templates/dashboard/searchable_select_multiple.html350
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>