New version of OraMon.jsp with sortable tables.
Showing
4 changed files
with
498 additions
and
2 deletions
... | @@ -7,6 +7,7 @@ | ... | @@ -7,6 +7,7 @@ |
7 | <html> | 7 | <html> |
8 | <head> | 8 | <head> |
9 | <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> | 9 | <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> |
10 | <script src="sorttable.js"></script> | ||
10 | <title>OraMon Admin</title> | 11 | <title>OraMon Admin</title> |
11 | </head> | 12 | </head> |
12 | <body> | 13 | <body> |
... | @@ -111,7 +112,7 @@ function dataHandler() { | ... | @@ -111,7 +112,7 @@ function dataHandler() { |
111 | </div> | 112 | </div> |
112 | 113 | ||
113 | <div style="background-color: LightSteelBlue; padding: 10px;"> | 114 | <div style="background-color: LightSteelBlue; padding: 10px;"> |
114 | <table> | 115 | <table class="sortable"> |
115 | <tr> | 116 | <tr> |
116 | <th>Name</th> | 117 | <th>Name</th> |
117 | <th>Instance</th> | 118 | <th>Instance</th> |
... | @@ -165,7 +166,7 @@ function dataHandler() { | ... | @@ -165,7 +166,7 @@ function dataHandler() { |
165 | </div> | 166 | </div> |
166 | 167 | ||
167 | <div style="background-color: LightSteelBlue; padding: 10px;"> | 168 | <div style="background-color: LightSteelBlue; padding: 10px;"> |
168 | <table> | 169 | <table class="sortable"> |
169 | <tr> | 170 | <tr> |
170 | <th>Name</th> | 171 | <th>Name</th> |
171 | <th>Connection String</th> | 172 | <th>Connection String</th> | ... | ... |
No preview for this file type
JavaMonWeb/WebContent/sorttable.js
0 → 100644
1 | /* | ||
2 | SortTable | ||
3 | version 2 | ||
4 | 7th April 2007 | ||
5 | Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/ | ||
6 | |||
7 | Instructions: | ||
8 | Download this file | ||
9 | Add <script src="sorttable.js"></script> to your HTML | ||
10 | Add class="sortable" to any table you'd like to make sortable | ||
11 | Click on the headers to sort | ||
12 | |||
13 | Thanks to many, many people for contributions and suggestions. | ||
14 | Licenced as X11: http://www.kryogenix.org/code/browser/licence.html | ||
15 | This basically means: do what you want with it. | ||
16 | */ | ||
17 | |||
18 | |||
19 | var stIsIE = /*@cc_on!@*/false; | ||
20 | |||
21 | sorttable = { | ||
22 | init: function() { | ||
23 | // quit if this function has already been called | ||
24 | if (arguments.callee.done) return; | ||
25 | // flag this function so we don't do the same thing twice | ||
26 | arguments.callee.done = true; | ||
27 | // kill the timer | ||
28 | if (_timer) clearInterval(_timer); | ||
29 | |||
30 | if (!document.createElement || !document.getElementsByTagName) return; | ||
31 | |||
32 | sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/; | ||
33 | |||
34 | forEach(document.getElementsByTagName('table'), function(table) { | ||
35 | if (table.className.search(/\bsortable\b/) != -1) { | ||
36 | sorttable.makeSortable(table); | ||
37 | } | ||
38 | }); | ||
39 | |||
40 | }, | ||
41 | |||
42 | makeSortable: function(table) { | ||
43 | if (table.getElementsByTagName('thead').length == 0) { | ||
44 | // table doesn't have a tHead. Since it should have, create one and | ||
45 | // put the first table row in it. | ||
46 | the = document.createElement('thead'); | ||
47 | the.appendChild(table.rows[0]); | ||
48 | table.insertBefore(the,table.firstChild); | ||
49 | } | ||
50 | // Safari doesn't support table.tHead, sigh | ||
51 | if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0]; | ||
52 | |||
53 | if (table.tHead.rows.length != 1) return; // can't cope with two header rows | ||
54 | |||
55 | // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as | ||
56 | // "total" rows, for example). This is B&R, since what you're supposed | ||
57 | // to do is put them in a tfoot. So, if there are sortbottom rows, | ||
58 | // for backwards compatibility, move them to tfoot (creating it if needed). | ||
59 | sortbottomrows = []; | ||
60 | for (var i=0; i<table.rows.length; i++) { | ||
61 | if (table.rows[i].className.search(/\bsortbottom\b/) != -1) { | ||
62 | sortbottomrows[sortbottomrows.length] = table.rows[i]; | ||
63 | } | ||
64 | } | ||
65 | if (sortbottomrows) { | ||
66 | if (table.tFoot == null) { | ||
67 | // table doesn't have a tfoot. Create one. | ||
68 | tfo = document.createElement('tfoot'); | ||
69 | table.appendChild(tfo); | ||
70 | } | ||
71 | for (var i=0; i<sortbottomrows.length; i++) { | ||
72 | tfo.appendChild(sortbottomrows[i]); | ||
73 | } | ||
74 | delete sortbottomrows; | ||
75 | } | ||
76 | |||
77 | // work through each column and calculate its type | ||
78 | headrow = table.tHead.rows[0].cells; | ||
79 | for (var i=0; i<headrow.length; i++) { | ||
80 | // manually override the type with a sorttable_type attribute | ||
81 | if (!headrow[i].className.match(/\bsorttable_nosort\b/)) { // skip this col | ||
82 | mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/); | ||
83 | if (mtch) { override = mtch[1]; } | ||
84 | if (mtch && typeof sorttable["sort_"+override] == 'function') { | ||
85 | headrow[i].sorttable_sortfunction = sorttable["sort_"+override]; | ||
86 | } else { | ||
87 | headrow[i].sorttable_sortfunction = sorttable.guessType(table,i); | ||
88 | } | ||
89 | // make it clickable to sort | ||
90 | headrow[i].sorttable_columnindex = i; | ||
91 | headrow[i].sorttable_tbody = table.tBodies[0]; | ||
92 | dean_addEvent(headrow[i],"click", sorttable.innerSortFunction = function(e) { | ||
93 | |||
94 | if (this.className.search(/\bsorttable_sorted\b/) != -1) { | ||
95 | // if we're already sorted by this column, just | ||
96 | // reverse the table, which is quicker | ||
97 | sorttable.reverse(this.sorttable_tbody); | ||
98 | this.className = this.className.replace('sorttable_sorted', | ||
99 | 'sorttable_sorted_reverse'); | ||
100 | this.removeChild(document.getElementById('sorttable_sortfwdind')); | ||
101 | sortrevind = document.createElement('span'); | ||
102 | sortrevind.id = "sorttable_sortrevind"; | ||
103 | sortrevind.innerHTML = stIsIE ? ' <font face="webdings">5</font>' : ' ▴'; | ||
104 | this.appendChild(sortrevind); | ||
105 | return; | ||
106 | } | ||
107 | if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) { | ||
108 | // if we're already sorted by this column in reverse, just | ||
109 | // re-reverse the table, which is quicker | ||
110 | sorttable.reverse(this.sorttable_tbody); | ||
111 | this.className = this.className.replace('sorttable_sorted_reverse', | ||
112 | 'sorttable_sorted'); | ||
113 | this.removeChild(document.getElementById('sorttable_sortrevind')); | ||
114 | sortfwdind = document.createElement('span'); | ||
115 | sortfwdind.id = "sorttable_sortfwdind"; | ||
116 | sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾'; | ||
117 | this.appendChild(sortfwdind); | ||
118 | return; | ||
119 | } | ||
120 | |||
121 | // remove sorttable_sorted classes | ||
122 | theadrow = this.parentNode; | ||
123 | forEach(theadrow.childNodes, function(cell) { | ||
124 | if (cell.nodeType == 1) { // an element | ||
125 | cell.className = cell.className.replace('sorttable_sorted_reverse',''); | ||
126 | cell.className = cell.className.replace('sorttable_sorted',''); | ||
127 | } | ||
128 | }); | ||
129 | sortfwdind = document.getElementById('sorttable_sortfwdind'); | ||
130 | if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); } | ||
131 | sortrevind = document.getElementById('sorttable_sortrevind'); | ||
132 | if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); } | ||
133 | |||
134 | this.className += ' sorttable_sorted'; | ||
135 | sortfwdind = document.createElement('span'); | ||
136 | sortfwdind.id = "sorttable_sortfwdind"; | ||
137 | sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾'; | ||
138 | this.appendChild(sortfwdind); | ||
139 | |||
140 | // build an array to sort. This is a Schwartzian transform thing, | ||
141 | // i.e., we "decorate" each row with the actual sort key, | ||
142 | // sort based on the sort keys, and then put the rows back in order | ||
143 | // which is a lot faster because you only do getInnerText once per row | ||
144 | row_array = []; | ||
145 | col = this.sorttable_columnindex; | ||
146 | rows = this.sorttable_tbody.rows; | ||
147 | for (var j=0; j<rows.length; j++) { | ||
148 | row_array[row_array.length] = [sorttable.getInnerText(rows[j].cells[col]), rows[j]]; | ||
149 | } | ||
150 | /* If you want a stable sort, uncomment the following line */ | ||
151 | //sorttable.shaker_sort(row_array, this.sorttable_sortfunction); | ||
152 | /* and comment out this one */ | ||
153 | row_array.sort(this.sorttable_sortfunction); | ||
154 | |||
155 | tb = this.sorttable_tbody; | ||
156 | for (var j=0; j<row_array.length; j++) { | ||
157 | tb.appendChild(row_array[j][1]); | ||
158 | } | ||
159 | |||
160 | delete row_array; | ||
161 | }); | ||
162 | } | ||
163 | } | ||
164 | }, | ||
165 | |||
166 | guessType: function(table, column) { | ||
167 | // guess the type of a column based on its first non-blank row | ||
168 | sortfn = sorttable.sort_alpha; | ||
169 | for (var i=0; i<table.tBodies[0].rows.length; i++) { | ||
170 | text = sorttable.getInnerText(table.tBodies[0].rows[i].cells[column]); | ||
171 | if (text != '') { | ||
172 | if (text.match(/^-?[£$¤]?[\d,.]+%?$/)) { | ||
173 | return sorttable.sort_numeric; | ||
174 | } | ||
175 | // check for a date: dd/mm/yyyy or dd/mm/yy | ||
176 | // can have / or . or - as separator | ||
177 | // can be mm/dd as well | ||
178 | possdate = text.match(sorttable.DATE_RE) | ||
179 | if (possdate) { | ||
180 | // looks like a date | ||
181 | first = parseInt(possdate[1]); | ||
182 | second = parseInt(possdate[2]); | ||
183 | if (first > 12) { | ||
184 | // definitely dd/mm | ||
185 | return sorttable.sort_ddmm; | ||
186 | } else if (second > 12) { | ||
187 | return sorttable.sort_mmdd; | ||
188 | } else { | ||
189 | // looks like a date, but we can't tell which, so assume | ||
190 | // that it's dd/mm (English imperialism!) and keep looking | ||
191 | sortfn = sorttable.sort_ddmm; | ||
192 | } | ||
193 | } | ||
194 | } | ||
195 | } | ||
196 | return sortfn; | ||
197 | }, | ||
198 | |||
199 | getInnerText: function(node) { | ||
200 | // gets the text we want to use for sorting for a cell. | ||
201 | // strips leading and trailing whitespace. | ||
202 | // this is *not* a generic getInnerText function; it's special to sorttable. | ||
203 | // for example, you can override the cell text with a customkey attribute. | ||
204 | // it also gets .value for <input> fields. | ||
205 | |||
206 | if (!node) return ""; | ||
207 | |||
208 | hasInputs = (typeof node.getElementsByTagName == 'function') && | ||
209 | node.getElementsByTagName('input').length; | ||
210 | |||
211 | if (node.getAttribute("sorttable_customkey") != null) { | ||
212 | return node.getAttribute("sorttable_customkey"); | ||
213 | } | ||
214 | else if (typeof node.textContent != 'undefined' && !hasInputs) { | ||
215 | return node.textContent.replace(/^\s+|\s+$/g, ''); | ||
216 | } | ||
217 | else if (typeof node.innerText != 'undefined' && !hasInputs) { | ||
218 | return node.innerText.replace(/^\s+|\s+$/g, ''); | ||
219 | } | ||
220 | else if (typeof node.text != 'undefined' && !hasInputs) { | ||
221 | return node.text.replace(/^\s+|\s+$/g, ''); | ||
222 | } | ||
223 | else { | ||
224 | switch (node.nodeType) { | ||
225 | case 3: | ||
226 | if (node.nodeName.toLowerCase() == 'input') { | ||
227 | return node.value.replace(/^\s+|\s+$/g, ''); | ||
228 | } | ||
229 | case 4: | ||
230 | return node.nodeValue.replace(/^\s+|\s+$/g, ''); | ||
231 | break; | ||
232 | case 1: | ||
233 | case 11: | ||
234 | var innerText = ''; | ||
235 | for (var i = 0; i < node.childNodes.length; i++) { | ||
236 | innerText += sorttable.getInnerText(node.childNodes[i]); | ||
237 | } | ||
238 | return innerText.replace(/^\s+|\s+$/g, ''); | ||
239 | break; | ||
240 | default: | ||
241 | return ''; | ||
242 | } | ||
243 | } | ||
244 | }, | ||
245 | |||
246 | reverse: function(tbody) { | ||
247 | // reverse the rows in a tbody | ||
248 | newrows = []; | ||
249 | for (var i=0; i<tbody.rows.length; i++) { | ||
250 | newrows[newrows.length] = tbody.rows[i]; | ||
251 | } | ||
252 | for (var i=newrows.length-1; i>=0; i--) { | ||
253 | tbody.appendChild(newrows[i]); | ||
254 | } | ||
255 | delete newrows; | ||
256 | }, | ||
257 | |||
258 | /* sort functions | ||
259 | each sort function takes two parameters, a and b | ||
260 | you are comparing a[0] and b[0] */ | ||
261 | sort_numeric: function(a,b) { | ||
262 | aa = parseFloat(a[0].replace(/[^0-9.-]/g,'')); | ||
263 | if (isNaN(aa)) aa = 0; | ||
264 | bb = parseFloat(b[0].replace(/[^0-9.-]/g,'')); | ||
265 | if (isNaN(bb)) bb = 0; | ||
266 | return aa-bb; | ||
267 | }, | ||
268 | sort_alpha: function(a,b) { | ||
269 | if (a[0]==b[0]) return 0; | ||
270 | if (a[0]<b[0]) return -1; | ||
271 | return 1; | ||
272 | }, | ||
273 | sort_ddmm: function(a,b) { | ||
274 | mtch = a[0].match(sorttable.DATE_RE); | ||
275 | y = mtch[3]; m = mtch[2]; d = mtch[1]; | ||
276 | if (m.length == 1) m = '0'+m; | ||
277 | if (d.length == 1) d = '0'+d; | ||
278 | dt1 = y+m+d; | ||
279 | mtch = b[0].match(sorttable.DATE_RE); | ||
280 | y = mtch[3]; m = mtch[2]; d = mtch[1]; | ||
281 | if (m.length == 1) m = '0'+m; | ||
282 | if (d.length == 1) d = '0'+d; | ||
283 | dt2 = y+m+d; | ||
284 | if (dt1==dt2) return 0; | ||
285 | if (dt1<dt2) return -1; | ||
286 | return 1; | ||
287 | }, | ||
288 | sort_mmdd: function(a,b) { | ||
289 | mtch = a[0].match(sorttable.DATE_RE); | ||
290 | y = mtch[3]; d = mtch[2]; m = mtch[1]; | ||
291 | if (m.length == 1) m = '0'+m; | ||
292 | if (d.length == 1) d = '0'+d; | ||
293 | dt1 = y+m+d; | ||
294 | mtch = b[0].match(sorttable.DATE_RE); | ||
295 | y = mtch[3]; d = mtch[2]; m = mtch[1]; | ||
296 | if (m.length == 1) m = '0'+m; | ||
297 | if (d.length == 1) d = '0'+d; | ||
298 | dt2 = y+m+d; | ||
299 | if (dt1==dt2) return 0; | ||
300 | if (dt1<dt2) return -1; | ||
301 | return 1; | ||
302 | }, | ||
303 | |||
304 | shaker_sort: function(list, comp_func) { | ||
305 | // A stable sort function to allow multi-level sorting of data | ||
306 | // see: http://en.wikipedia.org/wiki/Cocktail_sort | ||
307 | // thanks to Joseph Nahmias | ||
308 | var b = 0; | ||
309 | var t = list.length - 1; | ||
310 | var swap = true; | ||
311 | |||
312 | while(swap) { | ||
313 | swap = false; | ||
314 | for(var i = b; i < t; ++i) { | ||
315 | if ( comp_func(list[i], list[i+1]) > 0 ) { | ||
316 | var q = list[i]; list[i] = list[i+1]; list[i+1] = q; | ||
317 | swap = true; | ||
318 | } | ||
319 | } // for | ||
320 | t--; | ||
321 | |||
322 | if (!swap) break; | ||
323 | |||
324 | for(var i = t; i > b; --i) { | ||
325 | if ( comp_func(list[i], list[i-1]) < 0 ) { | ||
326 | var q = list[i]; list[i] = list[i-1]; list[i-1] = q; | ||
327 | swap = true; | ||
328 | } | ||
329 | } // for | ||
330 | b++; | ||
331 | |||
332 | } // while(swap) | ||
333 | } | ||
334 | } | ||
335 | |||
336 | /* ****************************************************************** | ||
337 | Supporting functions: bundled here to avoid depending on a library | ||
338 | ****************************************************************** */ | ||
339 | |||
340 | // Dean Edwards/Matthias Miller/John Resig | ||
341 | |||
342 | /* for Mozilla/Opera9 */ | ||
343 | if (document.addEventListener) { | ||
344 | document.addEventListener("DOMContentLoaded", sorttable.init, false); | ||
345 | } | ||
346 | |||
347 | /* for Internet Explorer */ | ||
348 | /*@cc_on @*/ | ||
349 | /*@if (@_win32) | ||
350 | document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>"); | ||
351 | var script = document.getElementById("__ie_onload"); | ||
352 | script.onreadystatechange = function() { | ||
353 | if (this.readyState == "complete") { | ||
354 | sorttable.init(); // call the onload handler | ||
355 | } | ||
356 | }; | ||
357 | /*@end @*/ | ||
358 | |||
359 | /* for Safari */ | ||
360 | if (/WebKit/i.test(navigator.userAgent)) { // sniff | ||
361 | var _timer = setInterval(function() { | ||
362 | if (/loaded|complete/.test(document.readyState)) { | ||
363 | sorttable.init(); // call the onload handler | ||
364 | } | ||
365 | }, 10); | ||
366 | } | ||
367 | |||
368 | /* for other browsers */ | ||
369 | window.onload = sorttable.init; | ||
370 | |||
371 | // written by Dean Edwards, 2005 | ||
372 | // with input from Tino Zijdel, Matthias Miller, Diego Perini | ||
373 | |||
374 | // http://dean.edwards.name/weblog/2005/10/add-event/ | ||
375 | |||
376 | function dean_addEvent(element, type, handler) { | ||
377 | if (element.addEventListener) { | ||
378 | element.addEventListener(type, handler, false); | ||
379 | } else { | ||
380 | // assign each event handler a unique ID | ||
381 | if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++; | ||
382 | // create a hash table of event types for the element | ||
383 | if (!element.events) element.events = {}; | ||
384 | // create a hash table of event handlers for each element/event pair | ||
385 | var handlers = element.events[type]; | ||
386 | if (!handlers) { | ||
387 | handlers = element.events[type] = {}; | ||
388 | // store the existing event handler (if there is one) | ||
389 | if (element["on" + type]) { | ||
390 | handlers[0] = element["on" + type]; | ||
391 | } | ||
392 | } | ||
393 | // store the event handler in the hash table | ||
394 | handlers[handler.$$guid] = handler; | ||
395 | // assign a global event handler to do all the work | ||
396 | element["on" + type] = handleEvent; | ||
397 | } | ||
398 | }; | ||
399 | // a counter used to create unique IDs | ||
400 | dean_addEvent.guid = 1; | ||
401 | |||
402 | function removeEvent(element, type, handler) { | ||
403 | if (element.removeEventListener) { | ||
404 | element.removeEventListener(type, handler, false); | ||
405 | } else { | ||
406 | // delete the event handler from the hash table | ||
407 | if (element.events && element.events[type]) { | ||
408 | delete element.events[type][handler.$$guid]; | ||
409 | } | ||
410 | } | ||
411 | }; | ||
412 | |||
413 | function handleEvent(event) { | ||
414 | var returnValue = true; | ||
415 | // grab the event object (IE uses a global event object) | ||
416 | event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event); | ||
417 | // get a reference to the hash table of event handlers | ||
418 | var handlers = this.events[event.type]; | ||
419 | // execute each event handler | ||
420 | for (var i in handlers) { | ||
421 | this.$$handleEvent = handlers[i]; | ||
422 | if (this.$$handleEvent(event) === false) { | ||
423 | returnValue = false; | ||
424 | } | ||
425 | } | ||
426 | return returnValue; | ||
427 | }; | ||
428 | |||
429 | function fixEvent(event) { | ||
430 | // add W3C standard event methods | ||
431 | event.preventDefault = fixEvent.preventDefault; | ||
432 | event.stopPropagation = fixEvent.stopPropagation; | ||
433 | return event; | ||
434 | }; | ||
435 | fixEvent.preventDefault = function() { | ||
436 | this.returnValue = false; | ||
437 | }; | ||
438 | fixEvent.stopPropagation = function() { | ||
439 | this.cancelBubble = true; | ||
440 | } | ||
441 | |||
442 | // Dean's forEach: http://dean.edwards.name/base/forEach.js | ||
443 | /* | ||
444 | forEach, version 1.0 | ||
445 | Copyright 2006, Dean Edwards | ||
446 | License: http://www.opensource.org/licenses/mit-license.php | ||
447 | */ | ||
448 | |||
449 | // array-like enumeration | ||
450 | if (!Array.forEach) { // mozilla already supports this | ||
451 | Array.forEach = function(array, block, context) { | ||
452 | for (var i = 0; i < array.length; i++) { | ||
453 | block.call(context, array[i], i, array); | ||
454 | } | ||
455 | }; | ||
456 | } | ||
457 | |||
458 | // generic enumeration | ||
459 | Function.prototype.forEach = function(object, block, context) { | ||
460 | for (var key in object) { | ||
461 | if (typeof this.prototype[key] == "undefined") { | ||
462 | block.call(context, object[key], key, object); | ||
463 | } | ||
464 | } | ||
465 | }; | ||
466 | |||
467 | // character enumeration | ||
468 | String.forEach = function(string, block, context) { | ||
469 | Array.forEach(string.split(""), function(chr, index) { | ||
470 | block.call(context, chr, index, string); | ||
471 | }); | ||
472 | }; | ||
473 | |||
474 | // globally resolve forEach enumeration | ||
475 | var forEach = function(object, block, context) { | ||
476 | if (object) { | ||
477 | var resolve = Object; // default | ||
478 | if (object instanceof Function) { | ||
479 | // functions have a "length" property | ||
480 | resolve = Function; | ||
481 | } else if (object.forEach instanceof Function) { | ||
482 | // the object implements a custom forEach method so use that | ||
483 | object.forEach(block, context); | ||
484 | return; | ||
485 | } else if (typeof object == "string") { | ||
486 | // the object is a string | ||
487 | resolve = String; | ||
488 | } else if (typeof object.length == "number") { | ||
489 | // the object is array-like | ||
490 | resolve = Array; | ||
491 | } | ||
492 | resolve.forEach(object, block, context); | ||
493 | } | ||
494 | }; | ||
495 |
No preview for this file type
-
Please register or sign in to post a comment