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