5fa8993e13fcb09d854193b534f964d127178c4c
[pharos-tools.git] / dashboard / src / templates / dashboard / searchable_select_multiple.html
1 <script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
2
3 <div class="autocomplete">
4     <div id="warning_pane" style="background: #FFFFFF; color: #CC0000;">
5         {% if incompatible == "true" %}
6         <h3>Warning: Incompatible Configuration</h3>
7         <p>Please make a different selection, as the current config conflicts with the selected pod</p>
8         {% endif %}
9     </div>
10
11     <div id="added_list">
12
13     </div>
14     <div id="added_counter">
15         <p id="added_number">0</p>
16         <p id="addable_limit">/ {% if selectable_limit > -1 %} {{ selectable_limit }} {% else %} &infin; {% endif %}added</p>
17     </div>
18
19     <input id="user_field" name="ignore_this" class="form-control" autocomplete="off" type="text" placeholder="{{placeholder}}" value="" oninput="search(this.value)"
20     {% if disabled %} disabled {% endif %}
21     >
22     </input>
23
24     <input type="hidden" id="selector" name="{{ name }}" class="form-control" style="display: none;"
25     {% if disabled %} disabled {% endif %}
26     >
27     </input>
28
29     <div id="scroll_restrictor">
30         <ul id="drop_results"></ul>
31     </div>
32     <style>
33         #scroll_restrictor {
34             flex: 1;
35             position: relative;
36             overflow-y: auto;
37             padding-bottom: 10px;
38         }
39         .autocomplete {
40             display: flex;
41             flex: 1;
42             flex-direction: column;
43         }
44         #user_field {
45             font-size: 14pt;
46             padding: 5px;
47
48         }
49
50         #drop_results{
51             list-style-type: none;
52             padding: 0;
53             margin: 0;
54             min-height: 0;
55             border: solid 1px #ddd;
56             border-top: none;
57             border-bottom: none;
58             visibility: hidden;
59             flex: 1;
60
61             position: absolute;
62             width: 100%;
63
64         }
65
66         #drop_results li a{
67             font-size: 14pt;
68             background-color: #f6f6f6;
69             padding: 7px;
70             text-decoration: none;
71             display: block;
72             overflow: hidden;
73             text-overflow: ellipsis;
74             white-space: nowrap;
75         }
76
77         #drop_results li a {
78             border-bottom: 1px solid #ddd;
79         }
80
81         .list_entry {
82             border: 1px solid #ccc;
83             border-radius: 5px;
84             margin-top: 5px;
85             vertical-align: middle;
86             line-height: 40px;
87             height: 40px;
88             padding-left: 12px;
89             width: 100%;
90             display: flex;
91         }
92
93         #drop_results li a:hover{
94             background-color: #ffffff;
95         }
96
97         .added_entry_text {
98             white-space: nowrap;
99             overflow: hidden;
100             text-overflow: ellipsis;
101             display: inline;
102             width: 100%;
103         }
104
105         .btn-remove {
106             float: right;
107             height: 30px;
108             margin: 4px;
109             padding: 1px;
110             max-width: 20%;
111             width: 15%;
112             min-width: 70px;
113             overflow: hidden;
114             text-overflow: ellipsis;
115         }
116
117         .entry_tooltip {
118             display: none;
119         }
120
121         #drop_results li a:hover .entry_tooltip {
122             position: absolute;
123             background: #444;
124             color: #ddd;
125             text-align: center;
126             font-size: 12pt;
127             border-radius: 3px;
128
129         }
130
131         #drop_results {
132             max-width: 100%;
133             display: inline-block;
134             list-style-type: none;
135             overflow: hidden;
136             white-space: nowrap;
137             text-overflow: ellipsis;
138         }
139
140         #drop_results li {
141             overflow: hidden;
142             text-overflow: ellipsis;
143         }
144
145         #added_counter {
146             text-align: center;
147         }
148
149         #added_number, #addable_limit {
150             display: inline;
151         }
152     </style>
153 </div>
154
155 <script type="text/javascript">
156     //flags
157     var show_from_noentry = {{show_from_noentry|yesno:"true,false"}}; // whether to show any results before user starts typing
158     var show_x_results = {{show_x_results|default:-1}}; // how many results to show at a time, -1 shows all results
159     var results_scrollable = {{results_scrollable|yesno:"true,false"}}; // whether list should be scrollable
160     var selectable_limit = {{selectable_limit|default:-1}}; // how many selections can be made, -1 allows infinitely many
161     var placeholder = "{{placeholder|default:"begin typing"}}"; // placeholder that goes in text box
162
163     //needed info
164     var items = {{items|safe}} // items to add to trie. Type is a dictionary of dictionaries with structure:
165         /*
166         {
167             id# : {
168                 "id": any, identifiable on backend
169                 "small_name": string, displayed first (before separator), searchable (use for e.g. username)
170                 "expanded_name": string, displayed second (after separator), searchable (use for e.g. email address)
171                 "string": string, not displayed, still searchable
172             }
173         }
174         */
175
176     /* used later:
177     {{ selectable_limit }}: changes what number displays for field
178     {{ name }}: form identifiable name, relevant for backend
179         // when submitted, form will contain field data in post with name as the key
180     {{ placeholder }}: "greyed out" contents put into search field initially to guide user as to what they're searching for
181     {{ initial }}: in search_field_init(), marked safe, an array of id's each referring to an id from items
182     */
183
184     //tries
185     var expanded_name_trie = {}
186     expanded_name_trie.isComplete = false;
187     var small_name_trie = {}
188     small_name_trie.isComplete = false;
189     var string_trie = {}
190     string_trie.isComplete = false;
191
192     var added_items = [];
193
194     search_field_init();
195
196     if( show_from_noentry )
197     {
198         search("");
199     }
200
201     function disable() {
202         var textfield = document.getElementById("user_field");
203         var drop = document.getElementById("drop_results");
204
205         textfield.disabled = "True";
206         drop.style.display = "none";
207
208         var btns = document.getElementsByClassName("btn-remove");
209         for( var i = 0; i < btns.length; i++ )
210         {
211             btns[i].classList.add("disabled");
212         }
213     }
214
215     function search_field_init() {
216         build_all_tries(items);
217
218         var initial = {{ initial|safe }};
219
220         for( var i = 0; i < initial.length; i++)
221         {
222             select_item(String(initial[i]));
223         }
224         if(initial.length == 1)
225         {
226             search(items[initial[0]]["small_name"]);
227             document.getElementById("user_field").value = items[initial[0]]["small_name"];
228         }
229     }
230
231     function build_all_tries(dict)
232     {
233         for( var i in dict )
234         {
235             add_item(dict[i]);
236         }
237     }
238
239     function add_item(item)
240     {
241         var id = item['id'];
242         add_to_tree(item['expanded_name'], id, expanded_name_trie);
243         add_to_tree(item['small_name'], id, small_name_trie);
244         add_to_tree(item['string'], id, string_trie);
245     }
246
247     function add_to_tree(str, id, trie)
248     {
249         inner_trie = trie;
250         while( str )
251         {
252             if( !inner_trie[str.charAt(0)] )
253             {
254                 new_trie = {};
255                 inner_trie[str.charAt(0)] = new_trie;
256             }
257             else
258             {
259                 new_trie = inner_trie[str.charAt(0)];
260             }
261
262             if( str.length == 1 )
263             {
264                 new_trie.isComplete = true;
265                 if( !new_trie.ids )
266                 {
267                     new_trie.ids = [];
268                 }
269                 new_trie.ids.push(id);
270             }
271             inner_trie = new_trie;
272             str = str.substring(1);
273         }
274     }
275
276     function search(input)
277     {
278         if( input.length == 0 && !show_from_noentry){
279             dropdown([]);
280             return;
281         }
282         else if( input.length == 0 && show_from_noentry)
283         {
284             dropdown(items); //show all items
285         }
286         else
287         {
288             var trees = []
289             var tr1 = getSubtree(input, expanded_name_trie);
290             trees.push(tr1);
291             var tr2 = getSubtree(input, small_name_trie);
292             trees.push(tr2);
293             var tr3 = getSubtree(input, string_trie);
294             trees.push(tr3);
295             var results = collate(trees);
296             dropdown(results);
297         }
298     }
299
300     function getSubtree(input, given_trie)
301     {
302         /*
303         recursive function to return the trie accessed at input
304         */
305
306         if( input.length == 0 ){
307             return given_trie;
308         }
309
310         else{
311         var substr = input.substring(0, input.length - 1);
312         var last_char = input.charAt(input.length-1);
313         var subtrie = getSubtree(substr, given_trie);
314         if( !subtrie ) //substr not in the trie
315         {
316             return {};
317         }
318         var indexed_trie = subtrie[last_char];
319         return indexed_trie;
320         }
321     }
322
323     function serialize(trie)
324     {
325         /*
326         takes in a trie and returns a list of its item id's
327         */
328         var itemIDs = [];
329         if ( !trie )
330         {
331             return itemIDs; //empty, base case
332         }
333         for( var key in trie )
334         {
335             if(key.length > 1)
336             {
337                 continue;
338             }
339             itemIDs = itemIDs.concat(serialize(trie[key]));
340         }
341         if ( trie.isComplete )
342         {
343             itemIDs.push(...trie.ids);
344         }
345
346         return itemIDs;
347     }
348
349     function collate(trees)
350     {
351         /*
352         takes a list of tries
353         returns a list of ids of objects that are available
354         */
355         results = [];
356         for( var i in trees )
357         {
358             var available_IDs = serialize(trees[i]);
359             for( var j=0; j<available_IDs.length; j++){
360                 var itemID = available_IDs[j];
361                 results[itemID] = items[itemID];
362             }
363         }
364         return results;
365     }
366
367     function generate_element_text(obj)
368     {
369         var content_strings = [obj['expanded_name'], obj['small_name'], obj['string']].filter(x => Boolean(x));
370         var result = content_strings.shift();
371         if( result == null || content_strings.length < 1) return result;
372         return result + " (" + content_strings.join(", ") + ")";
373     }
374
375     function dropdown(ids)
376     {
377         /*
378         takes in a mapping of ids to objects in  items
379         and displays them in the dropdown
380         */
381         var drop = document.getElementById("drop_results");
382         while(drop.firstChild)
383         {
384             drop.removeChild(drop.firstChild);
385         }
386
387         for( var id in ids )
388         {
389             var result_entry = document.createElement("li");
390             var result_button = document.createElement("a");
391             var obj = items[id];
392             var result_text = generate_element_text(obj);
393             result_button.appendChild(document.createTextNode(result_text));
394             result_button.setAttribute('onclick', 'select_item("' + obj['id'] + '")');
395             var tooltip = document.createElement("span");
396             var tooltiptext = document.createTextNode(result_text);
397             tooltip.appendChild(tooltiptext);
398             tooltip.setAttribute('class', 'entry_tooltip');
399             result_button.appendChild(tooltip);
400             result_entry.appendChild(result_button);
401             drop.appendChild(result_entry);
402         }
403
404         if( !drop.firstChild )
405         {
406             drop.style.visibility = 'hidden';
407         }
408         else
409         {
410             drop.style.visibility = 'inherit';
411         }
412     }
413
414     function select_item(item_id)
415     {
416         //TODO make faster
417         var item = items[item_id]['id'];
418         if( (selectable_limit > -1 && added_items.length < selectable_limit) || selectable_limit < 0 )
419         {
420             if( added_items.indexOf(item) == -1 )
421             {
422                 added_items.push(item);
423             }
424         }
425         update_selected_list();
426         document.getElementById("user_field").focus();
427     }
428
429     function remove_item(item_ref)
430     {
431         item = Object.values(items)[item_ref];
432         var index = added_items.indexOf(item);
433         added_items.splice(index, 1);
434
435         update_selected_list()
436         document.getElementById("user_field").focus();
437     }
438
439     function update_selected_list()
440     {
441         document.getElementById("added_number").innerText = added_items.length;
442         selector = document.getElementById('selector');
443         selector.value = JSON.stringify(added_items);
444         added_list = document.getElementById('added_list');
445
446         while(selector.firstChild)
447         {
448             selector.removeChild(selector.firstChild);
449         }
450         while(added_list.firstChild)
451         {
452             added_list.removeChild(added_list.firstChild);
453         }
454
455         list_html = "";
456
457         for( var key in added_items )
458         {
459             item_id = added_items[key];
460             item = items[item_id];
461
462             var element_entry_text = generate_element_text(item);
463
464             list_html += '<div class="list_entry">'
465                 + '<p class="added_entry_text">'
466                 + element_entry_text
467                 + '</p>'
468                 + '<button onclick="remove_item('
469                 + Object.values(items).indexOf(item)
470                 + ')" class="btn-remove btn">remove</button>';
471                 list_html += '</div>';
472         }
473
474         added_list.innerHTML = list_html;
475     }
476
477 </script>