diff options
Diffstat (limited to 'lib/VNWeb/TableOpts.pm')
-rw-r--r-- | lib/VNWeb/TableOpts.pm | 130 |
1 files changed, 99 insertions, 31 deletions
diff --git a/lib/VNWeb/TableOpts.pm b/lib/VNWeb/TableOpts.pm index fa976f40..42885fa1 100644 --- a/lib/VNWeb/TableOpts.pm +++ b/lib/VNWeb/TableOpts.pm @@ -31,6 +31,7 @@ package VNWeb::TableOpts; # # 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_num => 0/1, # Whether this is a numeric field, used in the UI to display "1→9" instead of "A→Z". # sort_default => 'asc', # Set to 'asc' or 'desc' if this column should be sorted on by default. # }, # popularity => { @@ -66,11 +67,11 @@ package VNWeb::TableOpts; use v5.26; use Carp 'croak'; use Exporter 'import'; -use TUWF; +use TUWF ':html5_'; use VNWeb::Auth; use VNWeb::HTML (); use VNWeb::Validation; -use VNWeb::Elm; +use VNWeb::JS; our @EXPORT = ('tableopts'); @@ -95,6 +96,7 @@ sub tableopts { views => [], # supported views, as numbers default => 0, # default settings, integer form ); + my @vis; while(@_) { my($k,$v) = (shift,shift); if($k eq '_views') { @@ -108,43 +110,63 @@ sub tableopts { $o{columns}{$k} = $v; $v->{id} = $k; push $o{col_order}->@*, $v; - $o{sort_ids}[$v->{sort_id}] = $v if defined $v->{sort_id}; + if(defined $v->{sort_id}) { + die "Duplicate sort_id $v->{sort_id}\n" if $o{sort_ids}[$v->{sort_id}]; + $o{sort_ids}[$v->{sort_id}] = $v; + } + die "Duplicate vis_id $v->{vis_id}\n" if defined $v->{vis_id} && $vis[$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]; + #warn "=== ".($o{pref}||'undef')."\n"; dump_ids(\%o); \%o } +# COMPAT: For old URLs, we assume that this validation is used on the 's' +# parameter, so we can accept two formats: +# - "s=$compat_sort_column/$order" +# - "s=$compat_sort_column&o=$order" +# In the latter case, the validation will use reqGet() to get the 'o' +# parameter. TUWF::set('custom_validations')->{tableopts} = sub { my($t) = @_; +{ onerror => sub { - my $d = $t->{pref} && auth ? tuwf->dbVali('SELECT', $t->{pref}, 'FROM users WHERE id =', \auth->uid) : undef; - bless([$d // $t->{default},$t], __PACKAGE__) + my $d = $t->{pref} && auth->pref($t->{pref}); + my $o = bless([$d // $t->{default},$t], __PACKAGE__); + $o->fixup; }, func => sub { my $obj = bless [undef, $t], __PACKAGE__; - my $col = [grep $_->{compat} && $_->{compat} eq $_[0], values $t->{columns}->%*]->[0]; + my($val,$ord) = $_[0] =~ m{^([^/]+)/([ad])$} ? ($1,$2) : ($_[0],undef); + my $col = [grep $_->{compat} && $_->{compat} eq $val, values $t->{columns}->%*]->[0]; if($col && defined $col->{sort_id}) { $obj->[0] = $t->{default}; $obj->set_sort_col_id($col->{sort_id}); - my $ord = tuwf->reqGet('o'); + $ord //= tuwf->reqGet('o'); $obj->set_order($ord && $ord eq 'd' ? 1 : 0); } else { $obj->[0] = _dec($_[0]) // return 0; } - $_[0] = $obj; + $_[0] = $obj->fixup; # We could do strict validation on the individual fields, but the methods below can handle incorrect data. 1; } } }; -sub query_encode { - my($v,$o) = $_[0]->@*; - $v == $o->{default} ? undef : _enc $v; +sub fixup { + my($obj) = @_; + # Reset sort_col and order to their default if the current sort_col id does not exist. + if(!$obj->[1]{sort_ids}[ $obj->sort_col_id ]) { + $obj->set_sort_col_id(sort_col_id([$obj->[1]{default}])); + $obj->set_order(order([$obj->[1]{default}])); + } + $obj } +sub query_encode { _enc $_[0][0] } + sub view { $views[$_[0][0] & 3] || $views[$_[0][1]{views}[0]] } sub rows { shift->view eq 'rows' } sub cards { shift->view eq 'cards' } @@ -156,7 +178,16 @@ sub order { $_[0][0] & 32 } sub set_order { if($_[1]) { $_[0][0] |= 32 } else { $_[0][0] &= ~32 } } sub sort_col_id { ($_[0][0] >> 6) & 63 } -sub set_sort_col_id { $_[0][0] = ($_[0][0] & (~1 - 0b111111000000)) | ($_[1] << 6) } +sub set_sort_col_id { $_[0][0] = ($_[0][0] & (~0 - 0b111111000000)) | ($_[1] << 6) } + +# Given a view id, return a new object with that view selected. +sub view_param { + my($self, $view) = @_; + my $n = bless [@$self], __PACKAGE__; + $n->[0] = ($n->[0] & ~3) | $view; + $n +} + # Given the key of a column, returns whether it is currently sorted on ('' / 'a' / 'd') sub sorted { @@ -177,7 +208,7 @@ sub sort_param { sub sql_order { my($self) = @_; my($v,$o) = $self->@*; - my $col = $o->{sort_ids}[ $self->sort_col_id ] || $o->{sort_ids}[ sort_col_id([$o->{default}]) ]; + my $col = $o->{sort_ids}[ $self->sort_col_id ]; die "No column to sort on" if !$col; my $order = $self->order ? 'DESC' : 'ASC'; my $opposite_order = $self->order ? 'ASC' : 'DESC'; @@ -187,43 +218,80 @@ sub sql_order { # Returns whether the given column key is visible. -sub vis { $_[0][0] & (1 << (12+$_[0][1]{columns}{$_[1]}{vis_id})) } +sub vis { my $c = $_[0][1]{columns}{$_[1]}; $c && defined $c->{vis_id} && ($_[0][0] & (1 << (12+$c->{vis_id}))) } + +# Given a list of column names, return a new object with only these columns visible +sub vis_param { + my($self, @cols) = @_; + my $n = bless [@$self], __PACKAGE__; + $n->[0] = $n->[0] & 0b1111_1111_1111; + $n->[0] |= 1 << (12+$self->[1]{columns}{$_}{vis_id}) for @cols; + $n; +} my $FORM_OUT = form_compile any => { - save => { required => 0 }, + save => { default => undef }, views => { type => 'array', values => { uint => 1 } }, - default => { uint => 1 }, value => { uint => 1 }, - sorts => { aoh => { id => { uint => 1 }, name => {} } }, + default => { uint => 1 }, + usaved => { uint => 1, default => undef }, + sorts => { aoh => { id => { uint => 1 }, name => {}, num => { anybool => 1 } } }, vis => { aoh => { id => { uint => 1 }, name => {} } }, }; -elm_api TableOptsSave => $FORM_OUT, { +js_api TableOptsSave => { save => { enum => ['tableopts_c', 'tableopts_v', 'tableopts_vt'] }, - value => { required => 0, uint => 1 } + value => { default => undef, uint => 1 } }, sub { my($f) = @_; - return elm_Unauth if !auth; - tuwf->dbExeci('UPDATE users SET', { $f->{save} => $f->{value} }, 'WHERE id =', \auth->uid); - elm_Success + return tuwf->resDenied if !auth; + tuwf->dbExeci('UPDATE users_prefs SET', { $f->{save} => $f->{value} }, 'WHERE id =', \auth->uid); + {} }; -sub elm_ { - my $self = shift; + +sub widget_ { + my($self,$url) = @_; my($v,$o) = $self->@*; - VNWeb::HTML::elm_ TableOpts => $FORM_OUT, { + menu_ class => 'tableopts', VNWeb::HTML::widget(TableOpts => $FORM_OUT, { save => auth ? $o->{pref} : undef, views => $o->{views}, - default => $o->{default}, value => $v, - sorts => [ map +{ id => $_->{sort_id}, name => $_->{name} }, grep defined $_->{sort_id}, values $o->{col_order}->@* ], + default => $o->{default}, + usaved => $o->{pref} && auth->pref($o->{pref}), + sorts => [ map +{ id => $_->{sort_id}, name => $_->{name}, num => $_->{sort_num}||0 }, grep defined $_->{sort_id}, values $o->{col_order}->@* ], vis => [ map +{ id => $_->{vis_id}, name => $_->{name} }, grep defined $_->{vis_id}, values $o->{col_order}->@* ], - }, sub { - TUWF::XML::div_ @_, sub { - TUWF::XML::input_ type => 'hidden', name => 's', value => $self->query_encode if defined $self->query_encode - } + }), sub { + li_ class => 'hidden', sub { + input_ type => 'hidden', name => 's', value => $self->query_encode; + }; + li_ sub { + a_ href => $url->(s => $self->view_param($_)), + class => $_ == ($self->[0] & 3) ? 'highlightselected' : undef, + title => ['List view', 'Card view', 'Grid view']->[$_], sub { + # SVG icons from https://lucide.dev/, MIT + lit_ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">'. + [ '<line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/>' + , '<rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><line x1="3" x2="21" y1="12" y2="12"/>' + , '<rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/><rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/>' + ]->[$_].'</g></svg>'; + }; + } for $o->{views}->@*; }; } + +# Helpful debugging function, dumps a quick overview of assigned numeric +# identifiers for the given opts. +sub dump_ids { + my($o) = @_; + warn sprintf "sort %2d %s %s\n", $_->{sort_id}, $_->{id}, $_->{name} + for sort { $a->{sort_id} <=> $b->{sort_id} } + grep defined $_->{sort_id}, values $o->{col_order}->@*; + warn sprintf "vis %2d %s %s\n", $_->{vis_id}, $_->{id}, $_->{name} + for sort { $a->{vis_id} <=> $b->{vis_id} } + grep defined $_->{vis_id}, values $o->{col_order}->@*; +} + 1; |