Implement sortable file listing tables.
authorNick Bowler <nbowler@draconx.ca>
Wed, 17 Feb 2021 02:03:20 +0000 (21:03 -0500)
committerNick Bowler <nbowler@draconx.ca>
Wed, 17 Feb 2021 03:39:39 +0000 (22:39 -0500)
Just for fun, let's add clickable headers to sort the file listings by
name, modification time, and file size, both in forward and reverse,
entirely implemented using XHTML and CSS.

content/images/down.svg [new file with mode: 0644]
content/images/up.svg [new file with mode: 0644]
content/style.scss
layouts/default.xsl
layouts/listing.xhtml

diff --git a/content/images/down.svg b/content/images/down.svg
new file mode 100644 (file)
index 0000000..95b82af
--- /dev/null
@@ -0,0 +1,200 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   sodipodi:docname="go-down.svg"
+   sodipodi:docbase="/home/tigert/cvs/freedesktop.org/tango-icon-theme/scalable/actions"
+   inkscape:version="0.46"
+   sodipodi:version="0.32"
+   id="svg11300"
+   height="48px"
+   width="48px"
+   inkscape:export-filename="/home/jimmac/Desktop/wi-fi.png"
+   inkscape:export-xdpi="90.000000"
+   inkscape:export-ydpi="90.000000"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape">
+  <defs
+     id="defs3">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 24 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="48 : 24 : 1"
+       inkscape:persp3d-origin="24 : 16 : 1"
+       id="perspective24" />
+    <linearGradient
+       id="linearGradient1442">
+      <stop
+         id="stop1444"
+         offset="0"
+         style="stop-color:#73d216" />
+      <stop
+         id="stop1446"
+         offset="1.0000000"
+         style="stop-color:#4e9a06" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient8662"
+       inkscape:collect="always">
+      <stop
+         id="stop8664"
+         offset="0"
+         style="stop-color:#000000;stop-opacity:1;" />
+      <stop
+         id="stop8666"
+         offset="1"
+         style="stop-color:#000000;stop-opacity:0;" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient8650"
+       inkscape:collect="always">
+      <stop
+         id="stop8652"
+         offset="0"
+         style="stop-color:#ffffff;stop-opacity:1;" />
+      <stop
+         id="stop8654"
+         offset="1"
+         style="stop-color:#ffffff;stop-opacity:0;" />
+    </linearGradient>
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient8662"
+       id="radialGradient1444"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.000000,0.000000,0.000000,0.536723,1.614716e-15,16.87306)"
+       cx="24.837126"
+       cy="36.421127"
+       fx="24.837126"
+       fy="36.421127"
+       r="15.644737" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient1442"
+       id="radialGradient1469"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.871885e-16,-0.843022,1.020168,2.265228e-16,0.606436,42.58614)"
+       cx="35.292667"
+       cy="20.494493"
+       fx="35.292667"
+       fy="20.494493"
+       r="16.956199" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient8650"
+       id="radialGradient1471"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(3.749427e-16,-2.046729,-1.557610,-2.853404e-16,44.11559,66.93275)"
+       cx="15.987216"
+       cy="1.5350308"
+       fx="15.987216"
+       fy="1.5350308"
+       r="17.171415" />
+  </defs>
+  <sodipodi:namedview
+     inkscape:window-y="30"
+     inkscape:window-x="0"
+     inkscape:window-height="818"
+     inkscape:window-width="1280"
+     inkscape:showpageshadow="false"
+     inkscape:document-units="px"
+     inkscape:grid-bbox="true"
+     showgrid="false"
+     inkscape:current-layer="layer1"
+     inkscape:cy="23.239067"
+     inkscape:cx="15.972815"
+     inkscape:zoom="11.313708"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     borderopacity="0.25490196"
+     bordercolor="#666666"
+     pagecolor="#ffffff"
+     id="base"
+     fill="#4e9a06"
+     stroke="#4e9a06" />
+  <metadata
+     id="metadata4">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:creator>
+          <cc:Agent>
+            <dc:title>Jakub Steiner</dc:title>
+          </cc:Agent>
+        </dc:creator>
+        <dc:source>http://jimmac.musichall.cz</dc:source>
+        <cc:license
+           rdf:resource="http://creativecommons.org/licenses/publicdomain/" />
+        <dc:title>Go Down</dc:title>
+        <dc:subject>
+          <rdf:Bag>
+            <rdf:li>go</rdf:li>
+            <rdf:li>lower</rdf:li>
+            <rdf:li>down</rdf:li>
+            <rdf:li>arrow</rdf:li>
+            <rdf:li>pointer</rdf:li>
+            <rdf:li>&gt;</rdf:li>
+          </rdf:Bag>
+        </dc:subject>
+        <dc:contributor>
+          <cc:Agent>
+            <dc:title>Andreas Nilsson</dc:title>
+          </cc:Agent>
+        </dc:contributor>
+      </cc:Work>
+      <cc:License
+         rdf:about="http://creativecommons.org/licenses/publicdomain/">
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Reproduction" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Distribution" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
+      </cc:License>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:groupmode="layer"
+     inkscape:label="Layer 1"
+     id="layer1">
+    <path
+       transform="matrix(1.214466,0.000000,0.000000,0.595458,-6.163846,16.31275)"
+       d="M 40.481863 36.421127 A 15.644737 8.3968935 0 1 1  9.1923885,36.421127 A 15.644737 8.3968935 0 1 1  40.481863 36.421127 z"
+       sodipodi:ry="8.3968935"
+       sodipodi:rx="15.644737"
+       sodipodi:cy="36.421127"
+       sodipodi:cx="24.837126"
+       id="path8660"
+       style="opacity:0.20454545;color:#000000;fill:url(#radialGradient1444);fill-opacity:1.0000000;fill-rule:evenodd;stroke:none;stroke-width:1.0000000;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10.000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;visibility:visible;display:inline;overflow:visible"
+       sodipodi:type="arc" />
+    <g
+       id="g1464"
+       transform="matrix(-1.000000,0.000000,0.000000,-1.000000,47.02856,43.99921)">
+      <path
+         style="opacity:1.0000000;color:#000000;fill:url(#radialGradient1469);fill-opacity:1.0000000;fill-rule:evenodd;stroke:#3a7304;stroke-width:1.0000004;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10.000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+         d="M 14.519136,38.500000 L 32.524165,38.496094 L 32.524165,25.504468 L 40.519531,25.496656 L 23.374809,5.4992135 L 6.5285585,25.497284 L 14.524440,25.501074 L 14.519136,38.500000 z "
+         id="path8643"
+         sodipodi:nodetypes="cccccccc" />
+      <path
+         style="opacity:0.50802141;color:#000000;fill:url(#radialGradient1471);fill-opacity:1.0000000;fill-rule:evenodd;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10.000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;visibility:visible;display:inline;overflow:visible"
+         d="M 39.429889,24.993467 L 32.023498,25.005186 L 32.026179,37.998023 L 16.647623,37.98887 C 17.417545,19.64788 27.370272,26.995797 32.029282,16.341991 L 39.429889,24.993467 z "
+         id="path8645"
+         sodipodi:nodetypes="cccccc" />
+      <path
+         sodipodi:nodetypes="cccccccc"
+         id="path8658"
+         d="M 15.520704,37.496094 L 31.522109,37.500000 L 31.522109,24.507050 L 38.338920,24.491425 L 23.384644,7.0388396 L 8.6781173,24.495782 L 15.518018,24.501029 L 15.520704,37.496094 z "
+         style="opacity:0.48128340;color:#000000;fill:none;fill-opacity:1.0000000;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.0000004;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10.000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;visibility:visible;display:inline;overflow:visible" />
+    </g>
+  </g>
+</svg>
diff --git a/content/images/up.svg b/content/images/up.svg
new file mode 100644 (file)
index 0000000..54263df
--- /dev/null
@@ -0,0 +1,196 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   sodipodi:docname="go-up.svg"
+   sodipodi:docbase="/home/tigert/cvs/freedesktop.org/tango-icon-theme/scalable/actions"
+   inkscape:version="0.46"
+   sodipodi:version="0.32"
+   id="svg11300"
+   height="48px"
+   width="48px"
+   inkscape:export-filename="/home/jimmac/Desktop/wi-fi.png"
+   inkscape:export-xdpi="90.000000"
+   inkscape:export-ydpi="90.000000"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape">
+  <defs
+     id="defs3">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 24 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="48 : 24 : 1"
+       inkscape:persp3d-origin="24 : 16 : 1"
+       id="perspective23" />
+    <linearGradient
+       id="linearGradient2304">
+      <stop
+         id="stop2306"
+         offset="0"
+         style="stop-color:#73d216" />
+      <stop
+         id="stop2308"
+         offset="1.0000000"
+         style="stop-color:#4e9a06" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient8662"
+       inkscape:collect="always">
+      <stop
+         id="stop8664"
+         offset="0"
+         style="stop-color:#000000;stop-opacity:1;" />
+      <stop
+         id="stop8666"
+         offset="1"
+         style="stop-color:#000000;stop-opacity:0;" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient8650"
+       inkscape:collect="always">
+      <stop
+         id="stop8652"
+         offset="0"
+         style="stop-color:#ffffff;stop-opacity:1;" />
+      <stop
+         id="stop8654"
+         offset="1"
+         style="stop-color:#ffffff;stop-opacity:0;" />
+    </linearGradient>
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient8650"
+       id="radialGradient1438"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(-3.749427e-16,-2.046729,1.557610,-2.853404e-16,2.767009,66.93275)"
+       cx="24.53788"
+       cy="0.40010813"
+       fx="24.53788"
+       fy="0.40010813"
+       r="17.171415" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient2304"
+       id="radialGradient1441"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.871885e-16,-0.843022,1.020168,2.265228e-16,0.606436,42.58614)"
+       cx="11.319205"
+       cy="22.454971"
+       fx="11.319205"
+       fy="22.454971"
+       r="16.956199" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient8662"
+       id="radialGradient1444"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.000000,0.000000,0.000000,0.536723,1.614716e-15,16.87306)"
+       cx="24.837126"
+       cy="36.421127"
+       fx="24.837126"
+       fy="36.421127"
+       r="15.644737" />
+  </defs>
+  <sodipodi:namedview
+     inkscape:window-y="30"
+     inkscape:window-x="0"
+     inkscape:window-height="818"
+     inkscape:window-width="1280"
+     inkscape:showpageshadow="false"
+     inkscape:document-units="px"
+     inkscape:grid-bbox="true"
+     showgrid="false"
+     inkscape:current-layer="layer1"
+     inkscape:cy="25.620377"
+     inkscape:cx="9.6380363"
+     inkscape:zoom="13.059378"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     borderopacity="0.25490196"
+     bordercolor="#666666"
+     pagecolor="#ffffff"
+     id="base"
+     fill="#73d216"
+     stroke="#73d216" />
+  <metadata
+     id="metadata4">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:creator>
+          <cc:Agent>
+            <dc:title>Jakub Steiner</dc:title>
+          </cc:Agent>
+        </dc:creator>
+        <dc:source>http://jimmac.musichall.cz</dc:source>
+        <cc:license
+           rdf:resource="http://creativecommons.org/licenses/publicdomain/" />
+        <dc:title>Go Up</dc:title>
+        <dc:subject>
+          <rdf:Bag>
+            <rdf:li>go</rdf:li>
+            <rdf:li>higher</rdf:li>
+            <rdf:li>up</rdf:li>
+            <rdf:li>arrow</rdf:li>
+            <rdf:li>pointer</rdf:li>
+            <rdf:li>&gt;</rdf:li>
+          </rdf:Bag>
+        </dc:subject>
+        <dc:contributor>
+          <cc:Agent>
+            <dc:title>Andreas Nilsson</dc:title>
+          </cc:Agent>
+        </dc:contributor>
+      </cc:Work>
+      <cc:License
+         rdf:about="http://creativecommons.org/licenses/publicdomain/">
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Reproduction" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#Distribution" />
+        <cc:permits
+           rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
+      </cc:License>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:groupmode="layer"
+     inkscape:label="Layer 1"
+     id="layer1">
+    <path
+       transform="matrix(1.214466,0.000000,0.000000,0.595458,-6.163846,16.31275)"
+       d="M 40.481863 36.421127 A 15.644737 8.3968935 0 1 1  9.1923885,36.421127 A 15.644737 8.3968935 0 1 1  40.481863 36.421127 z"
+       sodipodi:ry="8.3968935"
+       sodipodi:rx="15.644737"
+       sodipodi:cy="36.421127"
+       sodipodi:cx="24.837126"
+       id="path8660"
+       style="opacity:0.29946521;color:#000000;fill:url(#radialGradient1444);fill-opacity:1.0000000;fill-rule:evenodd;stroke:none;stroke-width:1.0000000;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10.000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;visibility:visible;display:inline;overflow:visible"
+       sodipodi:type="arc" />
+    <path
+       sodipodi:nodetypes="cccccccc"
+       id="path8643"
+       d="M 14.491792,38.500000 L 32.469477,38.500000 L 32.469477,25.547437 L 40.500000,25.547437 L 23.374809,5.4992135 L 6.5285585,25.489471 L 14.497096,25.555762 L 14.491792,38.500000 z "
+       style="opacity:1.0000000;color:#000000;fill:url(#radialGradient1441);fill-opacity:1.0000000;fill-rule:evenodd;stroke:#3a7304;stroke-width:1.0000004;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10.000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+    <path
+       sodipodi:nodetypes="cccscc"
+       id="path8645"
+       d="M 7.5855237,25.03253 L 14.995821,25.03253 L 15.062422,31.594339 C 20.718034,20.593878 31.055517,22.749928 31.656768,15.966674 C 31.656768,15.966674 23.366938,6.4219692 23.366938,6.4219692 L 7.5855237,25.03253 z "
+       style="opacity:0.50802141;color:#000000;fill:url(#radialGradient1438);fill-opacity:1.0000000;fill-rule:evenodd;stroke:none;stroke-width:1.0000000;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10.000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;visibility:visible;display:inline;overflow:visible" />
+    <path
+       style="opacity:0.48128340;color:#000000;fill:none;fill-opacity:1.0000000;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.0000004;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10.000000;stroke-dasharray:none;stroke-dashoffset:0.0000000;stroke-opacity:1.0000000;visibility:visible;display:inline;overflow:visible"
+       d="M 15.602735,37.500000 L 31.502578,37.500000 L 31.502578,24.507050 L 38.311576,24.507050 L 23.361206,7.0700896 L 8.6546798,24.550470 L 15.475049,24.528373 L 15.602735,37.500000 z "
+       id="path8658"
+       sodipodi:nodetypes="cccccccc" />
+  </g>
+</svg>
index 0c56cc4bb4d8f31ac75650c5c1013d7550098a02..22df5d950a9ff8ef49c73aa3e014daec840018ae 100644 (file)
@@ -137,12 +137,97 @@ table.cc {
     }
 }
 
+$sortcols: name, date, size;
+@each $col in $sortcols {
+    #filelist-#{$col}-sort {
+        &:checked {
+            & ~ table.filelist {
+                /* Update table header state */
+                th.#{$col} {
+                    label~label {
+                        display: -moz-inline-box !important;
+                        display: inline-block !important;
+                    }
+                    label { display: none; }
+                }
+
+                /* Show only appropriate items from the sort body (forward) */
+                tbody+tbody>tr.#{$col} { display: table-row; }
+                tbody+tbody>tr { display: none; }
+            }
+
+            & ~ #filelist-#{$col}-rev:checked ~ table.filelist {
+                /* Show only appropriate items from sort body (reversed) */
+                tbody+tbody>tr.#{$col}rev { display: table-row; }
+                tbody+tbody>tr { display: none; }
+            }
+
+            /* Unhide associated checkbox for keyboard navigation */
+            & ~ #filelist-#{$col}-rev { display: block !important; }
+        }
+
+        &:focus ~ table.filelist th>label~label>span {
+            border: 1px dotted;
+            padding: 0;
+        }
+
+        display: block !important;
+        position: absolute;
+        z-index: -1;
+        opacity: 0;
+    }
+
+    #filelist-#{$col}-rev {
+        &:checked ~ table.filelist {
+            /* Update table header state */
+            th.#{$col} {
+                img+img {
+                    display: -moz-inline-box !important;
+                    display: inline !important;
+                }
+                img { display: none; }
+            }
+        }
+
+        &:focus ~ table.filelist th>label~label>img {
+            border: 1px dotted;
+            padding: 0;
+        }
+
+        position: absolute;
+        z-index: -2;
+        opacity: 0;
+    }
+}
+
+/* Enable the sorted tables only when non-default option is selected */
+#filelist-name-rev, #filelist-date-sort, #filelist-size-sort {
+    &:checked~table.filelist>tbody {
+        &+tbody { display: table-row-group !important; }
+        display: none;
+    }
+}
+
 table.filelist {
     &>tr>*:first-child, &>*>tr>*:first-child {
         &+td { min-width: 50%; }
         width: 0;
     }
 
+    th>label>* { padding: 1px; }
+    th>label, th>label>* {
+        white-space: nowrap;
+        vertical-align: middle;
+        display: -moz-inline-box;
+        display: inline-block;
+        cursor: pointer;
+    }
+    th img { margin-left: 0.5ex; }
+
+    tbody+tbody {
+        border-bottom: solid 1px $ruledefaultcolour;
+    }
+
     img {
         display: block;
         height: 1.5em;
index 075ad184a71b4dfe53a5f9b8d29f29b95b5c78b0..84dcf9df801d3a25c0f68a8d1d3b23964174b6a7 100644 (file)
@@ -31,7 +31,7 @@
 <xsl:output method='xml' encoding='UTF-8' indent='yes'
   doctype-public='-//W3C//DTD XHTML 1.1//EN'
   doctype-system='http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'
-  cdata-section-elements='style' />
+  cdata-section-elements='style script' />
 
 <xsl:param name='source-uri'
   select='"//git.draconx.ca/gitweb/homepage.git/"' />
index aad56081170f2d33949ca3db6614b4d6e1d6cfb7..406e42ba79ac2eaac0fba108141b4fa2ec6f0833 100644 (file)
@@ -30,50 +30,163 @@ files = {}
     next unless "#{d}/" == mydir
 
     if p =~ %r{/$}
-      sz = Dir.children(File.dirname(rep.raw_path)).length - 1
-      type = "DIR"
+      displaysize = Dir.children(File.dirname(rep.raw_path)).length - 1
+      size = displaysize - 1000000
+      type = :DIR
     else
-      sz = human_filesize(File.size(rep.raw_path))
+      size = File.size(rep.raw_path)
+      displaysize = human_filesize(size)
       type = nil
     end
 
     files[f] = {
-      mtime: if t then t.getutc.strftime "%Y-%m-%d %H:%M UTC" end,
-      size: sz,
+      sorttime: if t then t.to_f else 0.0 end,
+      displaytime: if t then t.getutc.strftime "%Y-%m-%d %H:%M UTC" end,
+      displaysize: displaysize,
+      size: size,
       type: type,
     }
   end
 end
 
 if @items["#{File.dirname(mydir)}/index.lst"]
-  files[".."] = { type: "UP" }
+  files[".."] = { type: :UP }
+end
+
+def render_entry(files, key)
+  f = files[key]
+  return <<~EOF
+    <td>#{if f[:type]
+      "<img alt='#{f[:type]}' width='16' height='16' src='#{case f[:type]
+            when :DIR; "/images/folder.svg"
+            when :UP;  "/images/return.svg"
+            else raise "no icon for filetype #{f[:type]}"
+            end}' />"
+    end}</td>
+    <td><a href='#{key}'>#{
+      if key == ".." then "[Parent Directory]" else key end
+    }</a></td>
+    <td>#{f[:displaytime]}</td>
+    <td>#{f[:displaysize]}</td>
+  EOF
 end
 %>
+<div>
+<script type='x'><![CDATA[]]x><!--]]></script>
+<input style='display: none' type='radio' name='filelist-sort' id='filelist-name-sort' checked='checked' />
+<input style='display: none' type='radio' name='filelist-sort' id='filelist-date-sort' />
+<input style='display: none' type='radio' name='filelist-sort' id='filelist-size-sort' />
+<input style='display: none' type='checkbox' id='filelist-name-rev' />
+<input style='display: none' type='checkbox' id='filelist-date-rev' />
+<input style='display: none' type='checkbox' id='filelist-size-rev' />
+<script type='x'>--></script>
 <table class='filelist'>
   <thead>
-    <tr><th /><th>Name</th><th>Last Modified</th><th>Size</th></tr>
+    <tr>
+      <th />
+      <th class='name'>
+        <label for='filelist-name-sort'><span>Name</span></label>
+        <script type='x'><![CDATA[]]x><!--]]></script>
+        <label for='filelist-name-rev' style='display: none'>
+          <span>Name</span>
+          <img alt='FWD' width='16' height='16' src='/images/down.svg' />
+          <img alt='REV' width='16' height='16' src='/images/up.svg' style='display: none' />
+        </label>
+        <script type='x'>--></script>
+      </th>
+      <th class='date'>
+        <label for='filelist-date-sort'><span>Last Modified</span></label>
+        <script type='x'><![CDATA[]]x><!--]]></script>
+        <label for='filelist-date-rev' style='display: none'>
+          <span>Last Modified</span>
+          <img alt='FWD' width='16' height='16' src='/images/down.svg' />
+          <img alt='REV' width='16' height='16' src='/images/up.svg' style='display: none' />
+        </label>
+        <script type='x'>--></script>
+      </th>
+      <th class='size'>
+        <label for='filelist-size-sort'><span>Size</span></label>
+        <script type='x'><![CDATA[]]x><!--]]></script>
+        <label for='filelist-size-rev' style='display: none'>
+          <span>Size</span>
+          <img alt='FWD' width='16' height='16' src='/images/down.svg' />
+          <img alt='REV' width='16' height='16' src='/images/up.svg' style='display: none' />
+        </label>
+        <script type='x'>--></script>
+      </th>
+    </tr>
   </thead>
   <tbody>
-<% files.keys.sort{ |a, b| strverscmp(a, b) }.each do |key| %>
-    <tr>
-      <td>
-<% if files[key][:type] %>
-        <div>
-          <img src='<%=
-            case files[key][:type]
-            when "DIR"; "/images/folder.svg"
-            when "UP";  "/images/return.svg"
-            else raise "no icon for filetype #{files[key][:type]}"
-            end %>' alt='<%= files[key][:type] %>' width='16' height='16' />
-        </div>
+<%=
+    parentrow = if files[".."] then "#{render_entry(files, "..")}" end
+    files.delete("..")
+    if parentrow then "<tr>#{parentrow}</tr>" end
+%>
+<%
+by_name = files.keys.sort{ |a, b| strverscmp(a, b) }
+by_name.each_index do |i|
+  entry = render_entry(files, by_name[i])
+  if i+1 == by_name.length
+    entry.sub!(%r{(.*)</td>}m,
+               "\\1<script type='x'><![CDATA[]]x><!--]]></script></td>")
+  end
+%>
+    <tr><%= entry %></tr>
 <% end %>
-      </td>
-      <td><a href='<%= key %>'><%=
-        if key == ".." then "[Parent Directory]" else key end
-      %></a></td>
-      <td><%= files[key][:mtime] %></td>
-      <td><%= files[key][:size] %></td>
+  </tbody>
+
+  <tbody style='display: none'>
+<%
+def meta_cmp(files, key, a, b)
+  av, bv = files[a][key], files[b][key]
+  return av <=> bv if av != bv
+  return strverscmp(a, b)
+end
+
+by_date = files.keys.sort { |a, b| meta_cmp(files, :sorttime, a, b) }
+by_size = files.keys.sort { |a, b| meta_cmp(files, :size, a, b) }
+
+listnames = [ "namerev", "date", "daterev", "size", "sizerev" ]
+lists = [ by_name.reverse, by_date, by_date.reverse, by_size, by_size.reverse ]
+if parentrow
+%>
+  <tr class='<%= listnames.join(" ") %>'><%= parentrow %></tr>
+<%
+end
+evenmap = (0..(lists.length-1)).map { false }
+even = false
+
+while not (elems = lists.map(&:first)).compact.empty?
+  matches = (0..(lists.length-1)).to_a.keep_if { |x| evenmap[x] == even }
+  if !matches.empty?
+    elems = elems.values_at(*matches).compact
+    mode = elems.group_by{|a| a}.max{|a, b| a[1].length <=> b[1].length}[0]
+    matches = []
+
+    lists.each_index do |i|
+      if evenmap[i] == even and lists[i].first.eql? mode
+        lists[i].shift
+        evenmap[i] ^= true
+        evenmap[i] = nil if lists[i].empty?
+
+        matches << i
+      end
+    end
+%>
+    <tr class='<%= listnames.values_at(*matches).join(" ") %>'>
+      <%= render_entry(files, mode) %>
     </tr>
-<% end %>
+<%
+  else
+%>
+    <tr><td /></tr>
+<%
+  end
+
+  even ^= true
+end
+%>
+    <tr><td><script type='x'>--></script></tr>
   </tbody>
 </table>
+</div>