end
compile '/**/index.lst' do
- layout '/listing.xhtml'
- filter :erb
+ layout '/listing.erb'
layout '/default.xml'
layout '/default.xsl'
filter :relativize_paths, type: :xml
/*
* Nick's web site: default stylesheet
*
- * Copyright © 2018-2020 Nick Bowler
+ * Copyright © 2018-2021 Nick Bowler
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
thead>tr, tbody>tr { border: solid $ruledefaultcolour; }
th, thead>tr { border-bottom: 1px solid $rulestrongcolour; }
+tbody+tbody { border-bottom: 1px solid $ruledefaultcolour; }
*>table, *>th { border: none; }
thead>tr { border-width: 1px; }
tbody>tr { border-width: 0 1px; }
}
}
-$sortcols: name, date, size;
-@each $col in $sortcols {
- #filelist-#{$col}-sort {
+// CSS rules for stortable clicky table headers: Update the display of
+// the /table based on the current state. Each column has its own set
+// nearly-identical rules, only the class names differ.
+//
+// The clickytables.xsl stylesheet generates two inputs for each column.
+// These inputs are siblings of the table and all precede it in document
+// order. Moreover, the inputs for a column are ordered with respect to
+// each other, in this sequence:
+//
+// input.clicky-NAME -- checked iff NAME is selected for sorting.
+// input.clicky-NAME-rev -- checked to select reverse order.
+//
+// One of the column selection inputs will have a 'checked' attribute to
+// indicate the default order. This input is always first in document
+// order. No other inputs begin checked.
+//
+// The table itself consists of a thead (where the header labels are
+// located) and two tbody elements. The bulk of these rules relate
+// to updating the headers to visually indicate the current state.
+//
+// A sortable column's th element has the .clicky-NAME class, matching
+// its corresponding inputs, and has two label children. The first label
+// is visible only when the column is unselected, and is linked to the
+// .clicky-NAME input to activate that column. The second label is visible
+// only on the selected column and is linked to the .clicky-NAME-rev input
+// to toggle the reverse order.
+//
+// For the table body, the first tbody contains the default ordering
+// and is not styled by these rules (except to hide it when alternate
+// orderings are selected). The second tbody contains rows for all
+// alternate orderings, and is revealed by these rules. When revealed,
+// rows with the NAMEfwd or NAMErev class (for the forward and reverse
+// orderings, respectively) are shown and other rows are hidden.
+
+$clickynames: name, date, size;
+@each $col in $clickynames {
+ input.clicky-#{$col} {
&:checked {
- & ~ table.filelist {
- /* Update table header state */
- th.#{$col} {
+ &~table {
+ // Update table header state
+ & th.clicky-#{$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; }
+ // Show only appropriate items from the sort body (forward)
+ &>tbody+tbody>tr.#{$col}fwd { display: table-row; }
+ &>tbody+tbody>tr { display: none; }
+
+ // Unhide sort body
+ &>tbody {
+ &+tbody { display: table-row-group !important; }
+ 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; }
+ // reverse state for selected sort column
+ &~input.clicky-#{$col}-rev {
+ &:checked ~ table {
+ // Show only appropriate items from sort body (reversed)
+ &>tbody+tbody>tr.#{$col}rev { display: table-row; }
+ &>tbody+tbody>tr { display: none; }
+
+ // Unhide sort body
+ &>tbody {
+ &+tbody { display: table-row-group !important; }
+ display: none;
+ }
+ }
+
+ // Unhide to allow keyboard navigation to this input
+ display: block !important;
}
+ }
- /* Unhide associated checkbox for keyboard navigation */
- & ~ #filelist-#{$col}-rev { display: block !important; }
+ // If default input element is the only one selected, match it to
+ // return to default view (overriding the changes above). It is
+ // always the first input element among all the sibling elements.
+ // This seems to interoperate better than using the [checked] or
+ // :first-of-type selectors.
+ @at-root &:first-child, :not(input)+& {
+ &:checked~table>tbody {
+ &+tbody { display: none !important; }
+ display: table-row-group;
+ }
}
- &:focus ~ table.filelist th>label~label>span {
- border: 1px dotted;
- padding: 0;
+ &:focus ~ table th.clicky-#{$col}>label~label>span {
+ border-color: $foregroundcolour;
}
+ // Unhide to allow keyboard navigation
display: block !important;
+ pointer-events: none;
position: absolute;
- z-index: -1;
opacity: 0;
+ z-index: -1;
}
- #filelist-#{$col}-rev {
- &:checked ~ table.filelist {
- /* Update table header state */
- th.#{$col} {
+ input.clicky-#{$col}-rev {
+ &:checked ~ table {
+ // Update table header state
+ & th.clicky-#{$col} {
img+img {
display: -moz-inline-box !important;
- display: inline !important;
+ display: inline-block !important;
}
img { display: none; }
}
}
- &:focus ~ table.filelist th>label~label>img {
- border: 1px dotted;
- padding: 0;
+ &:focus ~ table th.clicky-#{$col}>label~label>img {
+ border-color: $foregroundcolour;
}
+ pointer-events: none;
position: absolute;
- z-index: -2;
opacity: 0;
+ z-index: -2;
}
-}
-/* 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;
+ th.clicky-#{$col}>label {
+ &, &>* {
+ white-space: nowrap;
+ vertical-align: middle;
+ display: -moz-inline-box;
+ display: inline-block;
+ cursor: pointer;
+ }
+
+ &>* { border: 1px dotted transparent; }
+
+ // Expand the first label a bit so the table (hopefully)
+ // does not reshape as columns are selected.
+ &:first-child {
+ margin-right: 1.75em;
+ padding-right: 2px;
+ }
+
+ &:active { color: $linkactivecolour; }
+ &:first-child:active>span, &~label:active>img {
+ border-color: $linkactivecolour;
+ }
+
+ img {
+ margin-left: 0.25em;
+ width: 1.5em;
+ height: auto;
+ }
}
}
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 {
+ tbody img {
display: block;
height: 1.5em;
width: auto;
--- /dev/null
+<?xml version='1.0' encoding='UTF-8' ?>
+<!--
+ Nick's web site: XHTML+CSS sortable clicky table headers.
+
+ Copyright © 2021 Nick Bowler
+
+ This implements the gory markup for clicky table headers, creating
+ input and related label elements to implement the clicking part.
+
+ To use: add the "clicky" attribute to each sortable header th element.
+ Class names corresponding to that header will be generated based on
+ the value of the attribute. For example, a header with clicky='date'
+ will add class="clicky-date" to the header and generate two input
+ elements: one with class="clicky-date" to indicate when that column
+ is selected for sorting, and another with class="clicky-date-rev" to
+ indicate when the reverse ordering is selected for that column.
+
+ The input table itself must have two tbody sections. The first is a
+ completely normal table body in the default sort order. The second
+ encodes all other possible orderings.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>
+-->
+<xsl:stylesheet version='1.0'
+ xmlns='http://www.w3.org/1999/xhtml'
+ xmlns:xhtml='http://www.w3.org/1999/xhtml'
+ xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>
+
+<xsl:template match='xhtml:table/@clicky|xhtml:th/@clicky' />
+<xsl:template match='xhtml:table[@clicky]'>
+ <xsl:variable name='group' select='generate-id(@clicky)' />
+ <xsl:variable name='clicky' select='@clicky' />
+
+ <div>
+ <script type='x'><![CDATA[]]x><!--]]></script>
+ <xsl:for-each select='xhtml:thead/*/xhtml:th/@clicky'>
+ <!-- hoist default element to be first in document order -->
+ <xsl:sort select='number(.!=$clicky)' data-type='number' />
+
+ <input style='display: none' id='sort-{generate-id(.)}'
+ type='radio' name='{$group}' class='clicky-{.}'>
+ <xsl:if test='.=$clicky'>
+ <xsl:attribute name='checked'>checked</xsl:attribute>
+ </xsl:if>
+ </input>
+ </xsl:for-each>
+ <xsl:for-each select='xhtml:thead/*/xhtml:th/@clicky'>
+ <input style='display: none' id='rev-{generate-id(.)}'
+ type='checkbox' class='clicky-{.}-rev' />
+ </xsl:for-each>
+ <script type='x'>--></script>
+
+ <xsl:copy>
+ <xsl:apply-templates select='node()|@*' />
+ </xsl:copy>
+ </div>
+</xsl:template>
+
+<xsl:template match='xhtml:table[@clicky]/xhtml:thead/*/xhtml:th[@clicky]'>
+ <xsl:copy>
+ <xsl:attribute name='class'>
+ <xsl:if test='@class'>
+ <xsl:value-of select='concat(@class, " ")' />
+ </xsl:if>
+ <xsl:value-of select='concat("clicky-", @clicky)' />
+ </xsl:attribute>
+ <xsl:apply-templates select='@*[local-name() != "class"]' />
+
+ <label for='sort-{generate-id(@clicky)}'>
+ <xsl:text>⁠</xsl:text>
+ <span><xsl:apply-templates select='node()' /></span>
+ </label>
+
+ <script type='x'><![CDATA[]]x><!--]]></script>
+ <label for='rev-{generate-id(@clicky)}' style='display: none'>
+ <xsl:text>⁠</xsl:text>
+ <span><xsl:apply-templates select='node()' /></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>
+ </xsl:copy>
+</xsl:template>
+
+<xsl:template match='xhtml:table[@clicky]/xhtml:tbody[last()]'>
+ <xsl:copy>
+ <xsl:attribute name='style'>
+ <xsl:if test='@style'>
+ <xsl:value-of select='concat(@style, "; ")' />
+ </xsl:if>
+ <xsl:text>display: none</xsl:text>
+ </xsl:attribute>
+ <xsl:apply-templates select='node()|@*[local-name() != "style"]' />
+ </xsl:copy>
+</xsl:template>
+
+<!-- Insert script hack around the second <tbody> -->
+<xsl:template match='xhtml:table[@clicky]/xhtml:tbody[1]/*[last()]/*[last()]'>
+ <xsl:copy>
+ <xsl:apply-templates select='node()|@*' />
+ <script type='x'><![CDATA[]]x><!--]]></script>
+ </xsl:copy>
+</xsl:template>
+
+<!-- Note that it is not allowed for <tr> to have no child elements. -->
+<xsl:template
+ match='xhtml:table[@clicky]/xhtml:tbody[last()]/*[last()]/*[last()]'>
+ <xsl:copy>
+ <xsl:apply-templates select='node()|@*' />
+ <script type='x'>--></script>
+ </xsl:copy>
+</xsl:template>
+
+</xsl:stylesheet>
<!--
Nick's web site: XHTML output stage
- Copyright © 2018-2020 Nick Bowler
+ Copyright © 2018-2021 Nick Bowler
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
</html>
</xsl:template>
+<xsl:include href='layouts/clickytable.xsl' />
+
</xsl:stylesheet>
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'>
+<table class='filelist' clicky='name'>
<thead>
<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>
+ <th clicky='name'>Name</th>
+ <th clicky='date'>Last Modified</th>
+ <th clicky='size'>Size</th>
</tr>
</thead>
<tbody>
%>
<%
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
+by_name.each do |key|
+ entry = render_entry(files, key)
%>
<tr><%= entry %></tr>
<% end %>
</tbody>
- <tbody style='display: none'>
+ <tbody>
<%
def meta_cmp(files, key, a, b)
av, bv = files[a][key], files[b][key]
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" ]
+listnames = [ "namerev", "datefwd", "daterev", "sizefwd", "sizerev" ]
lists = [ by_name.reverse, by_date, by_date.reverse, by_size, by_size.reverse ]
if parentrow
%>
even ^= true
end
%>
- <tr><td><script type='x'>--></script></tr>
</tbody>
</table>
-</div>