LP#1516867 Make HTML report tables sortable
authorDan Scott <dscott@laurentian.ca>
Tue, 17 Nov 2015 03:20:28 +0000 (22:20 -0500)
committerBen Shum <ben@evergreener.net>
Sun, 7 Feb 2016 21:01:52 +0000 (16:01 -0500)
Using the sortable JavaScript library from https://github.com/hubspot/sortable
(MIT license), we can make HTML reports much more usable by making them
sortable by clicking on the column headers. This should save some downloading
of CSV or Excel versions of the reports.

To test:

[1] Create or use a report template and generate an HTML report; ensure
    that the report has at most 10,000 rows of output.
[2] View the report output and click on one of the column headings;
    verify that the table sorts itself by that column's values.

Signed-off-by: Dan Scott <dscott@laurentian.ca>
Signed-off-by: Galen Charlton <gmc@esilibrary.com>
Signed-off-by: Ben Shum <ben@evergreener.net>
Open-ILS/src/reporter/clark-kent.pl
Open-ILS/web/js/sortable/LICENSE [new file with mode: 0644]
Open-ILS/web/js/sortable/sortable-theme-minimal.css [new file with mode: 0644]
Open-ILS/web/js/sortable/sortable.min.js [new file with mode: 0644]
docs/RELEASE_NOTES_NEXT/Administration/sortable_html_reports.adoc [new file with mode: 0644]

index 0e8a99b..758cb2c 100755 (executable)
@@ -553,18 +553,20 @@ sub build_html {
                                table { border-collapse: collapse; }
                                th { background-color: lightgray; }
                                td,th { border: solid black 1px; }
-                               * { font-family: sans-serif; font-size: 10px; }
+                               * { font-family: sans-serif; }
                        </style>
+                       <script src="/js/sortable/sortable.min.js"></script>
+                       <link rel="stylesheet" href="/js/sortable/sortable-theme-minimal.css" />
                CSS
 
-               print $raw "</head><body><table>";
+               print $raw "</head><body><table class='sortable-theme-minimal' data-sortable>";
 
                {       no warnings;
-                       print $raw "<tr><th>".join('</th><th>',@{$r->{column_labels}}).'</th></tr>';
-                       print $raw "<tr><td>".join('</td><td>',@$_                   ).'</td></tr>' for (@{$r->{data}});
+                       print $raw "<thead><tr><th>".join('</th><th>', @{$r->{column_labels}})."</th></tr></thead>\n<tbody>";
+                       print $raw "<tr><td>".join('</td><td>', @$_)."</td></tr>\n" for (@{$r->{data}});
                }
 
-               print $raw '</table></body></html>';
+               print $raw '</tbody></table></body></html>';
        
                $raw->close;
        }
diff --git a/Open-ILS/web/js/sortable/LICENSE b/Open-ILS/web/js/sortable/LICENSE
new file mode 100644 (file)
index 0000000..e19dfc9
--- /dev/null
@@ -0,0 +1,19 @@
+Copyright (C) 2013 Adam Schwartz, http://adamschwartz.co
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/Open-ILS/web/js/sortable/sortable-theme-minimal.css b/Open-ILS/web/js/sortable/sortable-theme-minimal.css
new file mode 100644 (file)
index 0000000..96db314
--- /dev/null
@@ -0,0 +1,55 @@
+/* line 2, ../sass/_sortable.sass */
+table[data-sortable] {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+/* line 6, ../sass/_sortable.sass */
+table[data-sortable] th {
+  vertical-align: bottom;
+  font-weight: bold;
+}
+/* line 10, ../sass/_sortable.sass */
+table[data-sortable] th, table[data-sortable] td {
+  text-align: left;
+  padding: 10px;
+}
+/* line 14, ../sass/_sortable.sass */
+table[data-sortable] th:not([data-sortable="false"]) {
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  -o-user-select: none;
+  user-select: none;
+  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+  -webkit-touch-callout: none;
+  cursor: pointer;
+}
+/* line 26, ../sass/_sortable.sass */
+table[data-sortable] th:after {
+  content: "";
+  visibility: hidden;
+  display: inline-block;
+  vertical-align: inherit;
+  height: 0;
+  width: 0;
+  border-width: 5px;
+  border-style: solid;
+  border-color: transparent;
+  margin-right: 1px;
+  margin-left: 10px;
+  float: right;
+}
+/* line 40, ../sass/_sortable.sass */
+table[data-sortable] th[data-sorted="true"]:after {
+  visibility: visible;
+}
+/* line 43, ../sass/_sortable.sass */
+table[data-sortable] th[data-sorted-direction="descending"]:after {
+  border-top-color: inherit;
+  margin-top: 8px;
+}
+/* line 47, ../sass/_sortable.sass */
+table[data-sortable] th[data-sorted-direction="ascending"]:after {
+  border-bottom-color: inherit;
+  margin-top: 3px;
+}
diff --git a/Open-ILS/web/js/sortable/sortable.min.js b/Open-ILS/web/js/sortable/sortable.min.js
new file mode 100644 (file)
index 0000000..8278f50
--- /dev/null
@@ -0,0 +1,2 @@
+/*! sortable.js 0.8.0 */
+(function(){var a,b,c,d,e,f,g;a="table[data-sortable]",d=/^-?[£$¤]?[\d,.]+%?$/,g=/^\s+|\s+$/g,c=["click"],f="ontouchstart"in document.documentElement,f&&c.push("touchstart"),b=function(a,b,c){return null!=a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent("on"+b,c)},e={init:function(b){var c,d,f,g,h;for(null==b&&(b={}),null==b.selector&&(b.selector=a),d=document.querySelectorAll(b.selector),h=[],f=0,g=d.length;g>f;f++)c=d[f],h.push(e.initTable(c));return h},initTable:function(a){var b,c,d,f,g,h;if(1===(null!=(h=a.tHead)?h.rows.length:void 0)&&"true"!==a.getAttribute("data-sortable-initialized")){for(a.setAttribute("data-sortable-initialized","true"),d=a.querySelectorAll("th"),b=f=0,g=d.length;g>f;b=++f)c=d[b],"false"!==c.getAttribute("data-sortable")&&e.setupClickableTH(a,c,b);return a}},setupClickableTH:function(a,d,f){var g,h,i,j,k,l;for(i=e.getColumnType(a,f),h=function(b){var c,g,h,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D;if(b.handled===!0)return!1;for(b.handled=!0,m="true"===this.getAttribute("data-sorted"),n=this.getAttribute("data-sorted-direction"),h=m?"ascending"===n?"descending":"ascending":i.defaultSortDirection,p=this.parentNode.querySelectorAll("th"),s=0,w=p.length;w>s;s++)d=p[s],d.setAttribute("data-sorted","false"),d.removeAttribute("data-sorted-direction");if(this.setAttribute("data-sorted","true"),this.setAttribute("data-sorted-direction",h),o=a.tBodies[0],l=[],m){for(D=o.rows,v=0,z=D.length;z>v;v++)g=D[v],l.push(g);for(l.reverse(),B=0,A=l.length;A>B;B++)k=l[B],o.appendChild(k)}else{for(r=null!=i.compare?i.compare:function(a,b){return b-a},c=function(a,b){return a[0]===b[0]?a[2]-b[2]:i.reverse?r(b[0],a[0]):r(a[0],b[0])},C=o.rows,j=t=0,x=C.length;x>t;j=++t)k=C[j],q=e.getNodeValue(k.cells[f]),null!=i.comparator&&(q=i.comparator(q)),l.push([q,k,j]);for(l.sort(c),u=0,y=l.length;y>u;u++)k=l[u],o.appendChild(k[1])}return"function"==typeof window.CustomEvent&&"function"==typeof a.dispatchEvent?a.dispatchEvent(new CustomEvent("Sortable.sorted",{bubbles:!0})):void 0},l=[],j=0,k=c.length;k>j;j++)g=c[j],l.push(b(d,g,h));return l},getColumnType:function(a,b){var c,d,f,g,h,i,j,k,l,m,n;if(d=null!=(l=a.querySelectorAll("th")[b])?l.getAttribute("data-sortable-type"):void 0,null!=d)return e.typesObject[d];for(m=a.tBodies[0].rows,h=0,j=m.length;j>h;h++)for(c=m[h],f=e.getNodeValue(c.cells[b]),n=e.types,i=0,k=n.length;k>i;i++)if(g=n[i],g.match(f))return g;return e.typesObject.alpha},getNodeValue:function(a){var b;return a?(b=a.getAttribute("data-value"),null!==b?b:"undefined"!=typeof a.innerText?a.innerText.replace(g,""):a.textContent.replace(g,"")):""},setupTypes:function(a){var b,c,d,f;for(e.types=a,e.typesObject={},f=[],c=0,d=a.length;d>c;c++)b=a[c],f.push(e.typesObject[b.name]=b);return f}},e.setupTypes([{name:"numeric",defaultSortDirection:"descending",match:function(a){return a.match(d)},comparator:function(a){return parseFloat(a.replace(/[^0-9.-]/g,""),10)||0}},{name:"date",defaultSortDirection:"ascending",reverse:!0,match:function(a){return!isNaN(Date.parse(a))},comparator:function(a){return Date.parse(a)||0}},{name:"alpha",defaultSortDirection:"ascending",match:function(){return!0},compare:function(a,b){return a.localeCompare(b)}}]),setTimeout(e.init,0),"function"==typeof define&&define.amd?define(function(){return e}):"undefined"!=typeof exports?module.exports=e:window.Sortable=e}).call(this);
\ No newline at end of file
diff --git a/docs/RELEASE_NOTES_NEXT/Administration/sortable_html_reports.adoc b/docs/RELEASE_NOTES_NEXT/Administration/sortable_html_reports.adoc
new file mode 100644 (file)
index 0000000..373418e
--- /dev/null
@@ -0,0 +1,5 @@
+Sortable HTML reports
+^^^^^^^^^^^^^^^^^^^^^
+HTML reports can now be sorted by clicking on the header for a given column.
+Clicking on the header toggles between sorting the column in ascending and
+descending order.