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