summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2021-02-22 11:25:43 +0100
committerYorhel <git@yorhel.nl>2021-02-22 11:26:05 +0100
commit8befb7606808e90d7e51011b5f60e425ba2c1eaf (patch)
tree5dc753bd06568447a3c9fa7d1b9bf629be5cabf1
parentdf817d02485046062195e15643c31081cb697031 (diff)
Chars::List: Add over-engineered TableOpts abstraction + crude options UI
The TableOpts abstraction is supposed to be used for other tables as well, eventually. It's overkill for the character list. It's also slightly more complex than necessary because I want shorter URLs.
-rw-r--r--elm/ColSelect.elm2
-rw-r--r--elm/TableOpts.elm95
-rw-r--r--lib/VNWeb/Chars/List.pm31
-rw-r--r--lib/VNWeb/Prelude.pm2
-rw-r--r--lib/VNWeb/TableOpts.pm159
5 files changed, 274 insertions, 15 deletions
diff --git a/elm/ColSelect.elm b/elm/ColSelect.elm
index 93c9a093..d78d0995 100644
--- a/elm/ColSelect.elm
+++ b/elm/ColSelect.elm
@@ -10,6 +10,8 @@
-- [ 'modified', 'Date modified' ],
-- ...
-- ] ]
+--
+-- TODO: Convert all uses of this module to the more flexible TableOpts.
module ColSelect exposing (main)
import Html exposing (..)
diff --git a/elm/TableOpts.elm b/elm/TableOpts.elm
new file mode 100644
index 00000000..65814185
--- /dev/null
+++ b/elm/TableOpts.elm
@@ -0,0 +1,95 @@
+module TableOpts exposing (main)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Browser
+import Bitwise as B
+import Lib.DropDown as DD
+import Lib.Api as Api
+import Lib.Html exposing (..)
+import Gen.TableOptsSave as GTO
+
+
+main : Program GTO.Recv Model Msg
+main = Browser.element
+ { init = \e -> (init e, Cmd.none)
+ , view = view
+ , update = update
+ , subscriptions = \model -> DD.sub model.dd
+ }
+
+type alias Model =
+ { opts : GTO.Recv
+ , dd : DD.Config Msg
+ , view : Int
+ , results : Int
+ , asc : Bool
+ , sort : Int
+ , cols : Int
+ }
+
+init : GTO.Recv -> Model
+init opts =
+ { opts = opts
+ , dd = DD.init "tableopts" Open
+ , view = B.and 3 opts.value
+ , results = B.and 7 (B.shiftRightBy 2 opts.value)
+ , asc = B.and 32 opts.value == 0
+ , sort = B.and 63 (B.shiftRightBy 6 opts.value)
+ , cols = B.shiftRightBy 12 opts.value
+ }
+
+
+type Msg
+ = Open Bool
+ | View Int Bool
+ | Results Int Bool
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg model =
+ case msg of
+ Open b -> ({ model | dd = DD.toggle model.dd b }, Cmd.none)
+ View n _ -> ({ model | view = n }, Cmd.none)
+ Results n _ -> ({ model | results = n }, Cmd.none)
+
+
+encBase64Alpha : Int -> String
+encBase64Alpha n = String.slice n (n+1) "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-"
+
+encBase64 : Int -> String
+encBase64 n = (if n >= 64 then encBase64 (n//64) else "") ++ encBase64Alpha (modBy 64 n)
+
+encInt : Model -> Int
+encInt m =
+ B.xor m.view
+ <| B.xor (B.shiftLeftBy 2 m.results)
+ <| B.xor (if m.asc then 0 else 32)
+ <| B.xor (B.shiftLeftBy 6 m.sort)
+ <| B.shiftLeftBy 12 m.cols
+
+view : Model -> Html Msg
+view model = div []
+ [ if encInt model == model.opts.default
+ then text ""
+ else input [ type_ "hidden", name "s", value (encBase64 (encInt model)) ] []
+ , DD.view model.dd Api.Normal
+ (text "display options")
+ (\_ -> [ table [ style "min-width" "300px" ]
+ [ tr [] [ td [] [ text "Format" ], td [] -- TODO: Icons, or some sort of preview?
+ [ linkRadio (model.view == 0) (View 0) [ text "Rows" ], text " / "
+ , linkRadio (model.view == 1) (View 1) [ text "Cards" ], text " / "
+ , linkRadio (model.view == 2) (View 2) [ text "Grid" ]
+ ] ]
+ , tr [] [ td [] [ text "Results" ], td []
+ [ linkRadio (model.results == 1) (Results 1) [ text "10" ], text " / "
+ , linkRadio (model.results == 2) (Results 2) [ text "25" ], text " / "
+ , linkRadio (model.results == 0) (Results 0) [ text "50" ], text " / "
+ , linkRadio (model.results == 3) (Results 3) [ text "100" ], text " / "
+ , linkRadio (model.results == 4) (Results 4) [ text "200" ]
+ ] ]
+ , tr [] [ td [] [], td [] [ input [ type_ "submit", class "submit", value "Update" ] [] ] ]
+ ]
+ ])
+ ]
diff --git a/lib/VNWeb/Chars/List.pm b/lib/VNWeb/Chars/List.pm
index f0bbc724..cefaad88 100644
--- a/lib/VNWeb/Chars/List.pm
+++ b/lib/VNWeb/Chars/List.pm
@@ -5,14 +5,15 @@ use VNWeb::AdvSearch;
use VNWeb::Filters;
use VNWeb::Images::Lib;
+our $TABLEOPTS = tableopts _views => [qw|rows cards grid|];
+
# Also used by VNWeb::TT::TraitPage
sub listing_ {
my($opt, $list, $count) = @_;
- $opt->{card} //= 0;
my sub url { '?'.query_encode %$opt, @_ }
- paginate_ \&url, $opt->{p}, [$count, 50], 't';
+ paginate_ \&url, $opt->{p}, [$count, $opt->{s}->results], 't', sub { $opt->{s}->elm_ };
div_ class => 'mainbox browse charb', sub {
table_ class => 'stripe', sub {
@@ -28,7 +29,7 @@ sub listing_ {
};
} for @$list;
}
- } if !$opt->{card};
+ } if $opt->{s}->rows;
div_ class => 'mainbox charbcard', sub {
my($w,$h) = (80,100);
@@ -50,7 +51,7 @@ sub listing_ {
};
};
} for @$list;
- } if $opt->{card} == 1;
+ } if $opt->{s}->cards;
div_ class => 'mainbox charbgrid', sub {
my($w,$h) = (160,200);
@@ -65,9 +66,9 @@ sub listing_ {
}
};
} for @$list;
- } if $opt->{card} == 2;
+ } if $opt->{s}->grid;
- paginate_ \&url, $opt->{p}, [$count, 50], 'b';
+ paginate_ \&url, $opt->{p}, [$count, $opt->{s}->results], 'b';
}
@@ -90,7 +91,7 @@ TUWF::get qr{/c(?:/(?<char>all|[a-z0]))?}, sub {
f => { advsearch_err => 'c' },
ch=> { onerror => [], type => 'array', scalar => 1, values => { onerror => undef, enum => ['0', 'a'..'z'] } },
fil => { required => 0 },
- card => { onerror => 0, enum => [0..2] }, # XXX: Experimental option, will be consolidated into a merged "display settings" field
+ s => { tableopts => $TABLEOPTS },
)->data;
$opt->{ch} = $opt->{ch}[0];
@@ -123,30 +124,30 @@ TUWF::get qr{/c(?:/(?<char>all|[a-z0]))?}, sub {
my($count, $list);
db_maytimeout {
$count = tuwf->dbVali('SELECT count(*) FROM chars c WHERE', $where);
- $list = $count ? tuwf->dbPagei({results => 50, page => $opt->{p}}, '
+ $list = $count ? tuwf->dbPagei({results => $opt->{s}->results(), page => $opt->{p}}, '
SELECT c.id, c.name, c.original, c.gender, c.image FROM chars c WHERE', $where, 'ORDER BY c.name, c.id'
) : [];
} || (($count, $list) = (undef, []));
enrich_listing $list;
- enrich_image_obj image => $list if $opt->{card};
+ enrich_image_obj image => $list if !$opt->{s}->rows;
$time = time - $time;
framework_ title => 'Browse characters', sub {
- div_ class => 'mainbox', sub {
- h1_ 'Browse characters';
- form_ action => '/c', method => 'get', sub {
+ form_ action => '/c', method => 'get', sub {
+ div_ class => 'mainbox', sub {
+ h1_ 'Browse characters';
searchbox_ c => $opt->{q}//'';
p_ class => 'browseopts', sub {
button_ type => 'submit', name => 'ch', value => ($_//''), ($_//'') eq ($opt->{ch}//'') ? (class => 'optselected') : (), !defined $_ ? 'ALL' : $_ ? uc $_ : '#'
- for (undef, 'a'..'z', 0);
+ for (undef, 'a'..'z', 0);
};
input_ type => 'hidden', name => 'ch', value => $opt->{ch}//'';
$opt->{f}->elm_;
advsearch_msg_ $count, $time;
};
- };
- listing_ $opt, $list, $count if $count;
+ listing_ $opt, $list, $count if $count;
+ }
};
};
diff --git a/lib/VNWeb/Prelude.pm b/lib/VNWeb/Prelude.pm
index a2bef7a5..ca7a423e 100644
--- a/lib/VNWeb/Prelude.pm
+++ b/lib/VNWeb/Prelude.pm
@@ -20,6 +20,7 @@
# use VNWeb::DB;
# use VNWeb::Validation;
# use VNWeb::Elm;
+# use VNWeb::TableOpts;
#
# + A few other handy tools.
#
@@ -64,6 +65,7 @@ sub import {
use VNWeb::DB;
use VNWeb::Validation;
use VNWeb::Elm;
+ use VNWeb::TableOpts;
1;
EOM;
diff --git a/lib/VNWeb/TableOpts.pm b/lib/VNWeb/TableOpts.pm
new file mode 100644
index 00000000..23f7fcee
--- /dev/null
+++ b/lib/VNWeb/TableOpts.pm
@@ -0,0 +1,159 @@
+package VNWeb::TableOpts;
+
+# This is a helper module to handle passing around various table display
+# options in a single compact query parameter.
+#
+# Supported options:
+#
+# Sort column & order
+# Number of results per page
+# View: rows, cards or grid
+# Which columns are visible
+#
+# Out of scope: pagination & filtering.
+#
+# Usage:
+#
+# my $config = tableopts
+# # Which views are supported (default: all)
+# _views => [ 'rows', 'cards', 'grid' ],
+#
+# # Column config.
+# # The key names are only used internally.
+# title => {
+# name => 'Title', # Column name, used in the configuration box.
+# compat => 'title', # Name of this column for compatibility with old URLs that referred to the column by name.
+# sort_id => 0, # This column can be sorted on, option indicates numeric identifier (must be stable)
+# sort_sql => 'v.title', # SQL to generate when sorting on this column,
+# # may include '?o' placeholder that will be replaced with selected ASC/DESC,
+# # or '!o' as placeholder for the opposite.
+# # If no placeholders are present, the ASC/DESC will be added automatically.
+# sort_default => 'asc', # Set to 'asc' or 'desc' if this column should be sorted on by default.
+# },
+# popularity => {
+# name => 'Popularity',
+# sort_id => 1,
+# sort_sql => 'v.c_popularity ?o, v.title',
+# vis_id => 0, # This column can be hidden/visible, option indicates numeric identifier
+# vis_default => 1, # If this column should be visible by default
+# };
+#
+# my $opts = tuwf->validate(get => s => { tableopts => $config })->data;
+#
+# my $sql = sql('.... ORDER BY', $opts->sql_order); (TODO)
+#
+# $opts->view; # Current view, 'rows', 'cards' or 'grid'
+# $opts->results; # How many results to display
+# $opts->vis('popularity'); # is the column visible? (TODO)
+#
+#
+#
+# Table options are encoded in a base64-encoded 31 bits integer (can be
+# extended, but bitwise operations in JS are quirky beyond 31 bits).
+# The bit layout is as follows, 0 being the least significant bit:
+#
+# 0 - 1: view 0: rows, 1: cards, 2: grid (3: unused)
+# 2 - 4: results 0: 50, 1: 10, 2: 25, 3: 100, 4: 200 (5-7: unused)
+# 5: order 0: ascending, 1: descending
+# 6 - 11: sort column, identifier used in the configuration
+# 12 - 31: column visibility, identifier in the configuration is used as bit index (12+$vis_id)
+#
+# This supports 64 column identifiers for sorting, 19 identifiers for visibility.
+
+use v5.26;
+use Carp 'croak';
+use Exporter 'import';
+use TUWF;
+use VNWeb::HTML ();
+use VNWeb::Validation;
+use VNWeb::Elm;
+
+our @EXPORT = ('tableopts');
+
+my @alpha = (0..9, 'a'..'z', 'A'..'Z', '_', '-');
+my %alpha = map +($alpha[$_],$_), 0..$#alpha;
+sub _enc { ($_[0] >= @alpha ? _enc(int $_[0]/@alpha) : '').$alpha[$_[0]%@alpha] }
+sub _dec { return if length $_[0] > 6; my $n = 0; $n = $n*@alpha + ($alpha{$_}//return) for split //, $_[0]; $n }
+
+my @views = qw|rows cards grid|;
+my %views = map +($views[$_], $_), 0..$#views;
+
+my @results = (50, 10, 25, 100, 200);
+my %results = map +($results[$_], $_), 0..$#results;
+
+
+# Turn config options into something more efficient to work with
+sub tableopts {
+ my %o = (
+ sort_ids => [], # identifier => column name
+ vis_ids => [], # identifier => column name
+ col_order => [], # column names in the order listed in the config
+ columns => {}, # column name => config hash
+ views => [], # supported views, as numbers
+ default => 0, # default settings, integer form
+ );
+ while(@_) {
+ my($k,$v) = (shift,shift);
+ if($k eq '_views') {
+ $o{views} = [ map $views{$_}//croak("unknown view: $_"), ref $v ? @$v : $v ];
+ next;
+ }
+ $o{columns}{$k} = $v;
+ push $o{col_order}->@*, $k;
+ $o{sort_ids}[$v->{sort_id}] = $k if defined $v->{sort_id};
+ $o{vis_ids}[$v->{vis_id}] = $k if defined $v->{vis_id};
+ $o{default} |= ($v->{sort_id} << 6) | ({qw|asc 0 desc 32|}->{$v->{sort_default}}//croak("unknown sort_default: $v->{sort_default}")) if $v->{sort_default};
+ $o{default} |= 1 << ($v->{vis_id} + 12) if $v->{vis_default};
+ }
+ $o{views} ||= [0];
+ $o{default} |= $o{views}[0];
+ \%o
+}
+
+
+TUWF::set('custom_validations')->{tableopts} = sub {
+ my($t) = @_;
+ +{ onerror => bless([$t->{default},$t], __PACKAGE__), func => sub {
+ # TODO: compatibility with the old ?s=<colname> sort parameter
+ my $v = _dec $_[0] or return 0;
+ # We could do strict validation on the individual fields, but the methods below can handle incorrect data.
+ $_[0] = bless [$v, $t], __PACKAGE__;
+ 1;
+ } }
+};
+
+sub query_encode {
+ my($v,$o) = $_[0]->@*;
+ $v == $o->{default} ? undef : _enc $v;
+}
+
+sub view { $views[$_[0][0] & 3] || $views[$_[0][1]{views}[0]] }
+sub rows { shift->view eq 'rows' }
+sub cards { shift->view eq 'cards' }
+sub grid { shift->view eq 'grid' }
+
+sub results { $results[($_[0][0] >> 2) & 7] || $results[0] }
+
+
+my $FORM_OUT = form_compile any => {
+ views => { type => 'array', values => { uint => 1 } },
+ default => { uint => 1 },
+ value => { uint => 1 },
+ # TODO: Sorting & column visibility
+};
+
+elm_api TableOptsSave => $FORM_OUT, {}, sub { ... };
+
+sub elm_ {
+ my $self = shift;
+ my($v,$o) = $self->@*;
+ VNWeb::HTML::elm_ TableOpts => $FORM_OUT, {
+ views => $o->{views},
+ default => $o->{default},
+ value => $v,
+ }, sub {
+ TUWF::XML::input_ type => 'hidden', name => 's', value => $self->query_encode if defined $self->query_encode
+ };
+}
+
+1;