Add optional counters and simplify multi-select toolbars

ⓘ Support count => 0, hide bulk actions below nine entries, and remove empty toolbar space.
This commit is contained in:
Ilia Ross
2026-09-09 13:40:08 +02:00
parent bbd9a56e42
commit c8249f9a50
3 changed files with 64 additions and 14 deletions

View File

@@ -391,7 +391,7 @@ like(main::ui_form_columns_table('x.cgi', [ [ 'go', 'Go' ] ], 0, undef, undef,
[ [ 'a', 'A' ],
{ 'value' => 'b', 'label' => 'B', 'suffix' => '.x',
'level' => 1, 'tag' => 'Plan' },
[ 'c', 'C' ] ],
[ 'c', 'C' ], map { [ $_, uc($_) ] } qw(d e f g h) ],
{ 'modes' => { 'name' => 'all', 'value' => 1,
'options' => [ [ 1, 'All' ], [ 0, 'Some' ] ],
'hide' => [ 1 ] } });
@@ -417,7 +417,7 @@ like(main::ui_form_columns_table('x.cgi', [ [ 'go', 'Go' ] ], 0, undef, undef,
'the links are laid out by ui_links_row');
unlike($html, qr/<br>\s*<span[^>]*ui_search/,
'without the line break that row ends with');
unlike(main::ui_multi_select_list('x', [ ], [ [ 'a', 'A' ] ], { 'disabled' => 1 }),
unlike(main::ui_multi_select_list('x', [ ], [ map { [ $_, $_ ] } 1..9 ], { 'disabled' => 1 }),
qr/select_all/, 'a disabled widget has no links');
like($html, qr/<select [^>]*name="all"/, 'modes are a select by default');
my ($hide) = $html =~ /data-ui-multi-hide="([^"]*)"/;
@@ -427,7 +427,7 @@ like(main::ui_form_columns_table('x.cgi', [ [ 'go', 'Go' ] ], 0, undef, undef,
'count of chosen entries next to the mode select');
like(main::ui_multi_select_list('x', [ 'a' ], [ [ 'a', 'A' ] ]),
qr/ui_multi_tools"[^>]*>(?:(?!ui_multi_list).)*<span (?=[^>]*\bui_multi_count\b)[^>]*>1 selected</s,
'count next to the links when there are no modes');
'count in the toolbar when there are no modes');
like(main::ui_multi_select_list('x', [ ], [ [ 'a', 'A' ] ]),
qr/<span (?=[^>]*\bui_multi_count\b)(?=[^>]*\bhidden\b)/,
'count hidden while nothing is chosen');
@@ -451,7 +451,50 @@ like(main::ui_form_columns_table('x.cgi', [ [ 'go', 'Go' ] ], 0, undef, undef,
like(main::ui_multi_select_list('g', [ 'a' ],
[ [ 'a', 'A' ], [ 'b', 'B' ] ], 5, 1, 1),
qr/value="a"[^>]*disabled/,
'disabled of ui_multi_select disables the rows');
'disabled of ui_multi_select_list disables the rows');
}
# Short pickers need neither bulk links nor the default filter.
{
foreach my $size (1, 8, 9) {
my $html = main::ui_multi_select_list('size', [],
[ map { [ $_, $_ ] } 1..$size ]);
is(scalar(() = $html =~ /data-ui-multi-action="(?:all|invert)"/g),
$size > 8 ? 2 : 0, "$size entries: bulk link threshold");
is(scalar(() = $html =~ /data-ui-multi-search="1"/g),
$size > 8 ? 1 : 0, "$size entries: default search threshold");
}
unlike(main::ui_multi_select_list('short_search', [], [ [ 'a', 'A' ] ],
{ 'search' => 1 }), qr/data-ui-multi-action="(?:all|invert)"/,
'explicit search does not add bulk links to a short list');
my $html = main::ui_multi_select_list('short_mode', [], [ [ 'a', 'A' ] ],
{ 'modes' => { 'name' => 'mode', 'value' => 0,
'options' => [ [ 0, 'Selected' ], [ 1, 'All' ] ] } });
unlike($html, qr/ui_multi_tools/, 'mode-only pickers omit the empty toolbar');
}
# Omitting the counter preserves selections and the remaining controls.
{
my $short = main::ui_multi_select_list('quiet', [ 'a' ], [ [ 'a', 'A' ] ],
{ 'count' => 0 });
unlike($short, qr/ui_multi_count|ui_multi_tools/,
'counter-free short lists have no empty toolbar');
like($short, qr/type='hidden'[^>]*name="quiet"[^>]*value="a"/,
'counter-free lists retain their submitted selection');
my $modes = main::ui_multi_select_list('quiet_mode', [ 'a' ], [ [ 'a', 'A' ] ],
{ 'count' => 0, 'modes' => { 'name' => 'mode', 'value' => 0,
'options' => [ [ 0, 'Selected' ], [ 1, 'All' ] ] } });
unlike($modes, qr/ui_multi_count|ui_multi_tools/,
'counter-free mode selectors have no counter or empty toolbar');
like($modes, qr/<select [^>]*name="mode"/,
'the mode selector remains available without a counter');
my $long = main::ui_multi_select_list('quiet_long', [ 1 ],
[ map { [ $_, $_ ] } 1..9 ], { 'count' => 0 });
unlike($long, qr/ui_multi_count/, 'long lists can also omit the counter');
like($long, qr/data-ui-multi-action="all"/,
'counter-free long lists retain bulk selection');
like($long, qr/data-ui-multi-search="1"/,
'counter-free long lists retain filtering');
}
# Legacy attributes must retain row presentation and checkbox behavior.

View File

@@ -5741,7 +5741,9 @@ Size, add-if-missing, titles and width are ignored.
=item opts - Optional hash reference with the keys :
=item search - Show or hide the filter button; defaults to on above eight entries. The input opens to its left in reserved space. Selection links affect visible, enabled entries and are omitted when disabled.
=item search - Show or hide the filter button; defaults to on above eight entries. The input opens to its left in reserved space. Selection links appear above eight entries, affect visible, enabled entries and are omitted when disabled.
=item count - Show the selection count; defaults to on. Set to 0 to omit it.
=item placeholder - Hint text of the filter box.
@@ -5845,12 +5847,15 @@ my $children = ref($opts->{'children'}) eq 'HASH' &&
$opts->{'children'} : undef;
my $folded = $children && $children->{'checked'} ? 1 : 0;
# Exclude folded children from the count, retaining their submitted values.
my $nchosen = grep { !$folded || !$_->{'level'} } @chosen;
my $cattrs = { 'class' => 'ui_multi_count' };
$cattrs->{'hidden'} = undef if (!$nchosen || $hidden);
my $count = &ui_tag('span',
&html_escape(&text('ui_multi_selected', $nchosen)), $cattrs);
my $count = '';
if (!defined($opts->{'count'}) || $opts->{'count'}) {
# Exclude folded children, retaining their submitted values.
my $nchosen = grep { !$folded || !$_->{'level'} } @chosen;
my $cattrs = { 'class' => 'ui_multi_count' };
$cattrs->{'hidden'} = undef if (!$nchosen || $hidden);
$count = &ui_tag('span',
&html_escape(&text('ui_multi_selected', $nchosen)), $cattrs);
}
my $counted = 0;
my $search = defined($opts->{'search'}) ? $opts->{'search'} : @items > 8;
@@ -5902,9 +5907,9 @@ if ($hasmodes) {
$counted = 1;
}
# Use themed selection links, with actions scoped to this list.
# Larger lists get themed selection links, with actions scoped to this list.
my $tools = "";
if (!$dis) {
if (!$dis && @items > 8) {
my $links = &ui_links_row([
&ui_tag('a', &html_escape($text{'ui_selall'}),
{ 'href' => '#', 'class' => 'select_all',
@@ -5947,7 +5952,7 @@ if ($search) {
$tools .= &ui_tag('span', $filter,
{ 'class' => 'ui_search ui_multi_filter' });
}
my $body = &ui_tag('div', $tools, { 'class' => 'ui_multi_tools' });
my $body = $tools ? &ui_tag('div', $tools, { 'class' => 'ui_multi_tools' }) : '';
# Render themed checkboxes with optional suffixes, child counts and tags.
my $rows = "";

View File

@@ -1069,6 +1069,8 @@ a.ui_list_link:hover { color: var(--ui-accent) !important; }
align-items: center;
gap: 4px 0;
}
/* A hidden count alone does not need a toolbar row. */
.ui_multi_tools:not(:has(> :not([hidden]))) { display: none; }
/* ui_links_row handles link spacing; add room for the count. */
.ui_multi_links { min-width: 0; }
.ui_multi_tools .ui_multi_count:not(:first-child) {