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