mirror of
https://github.com/webmin/webmin.git
synced 2026-10-27 21:50:40 +00:00
This PR updates the framed gray theme with a modern light and dark design while keeping its frameset and old class names without adding any extra complexity on Perl or JS side. The left menu becomes a floating panel with product logos, a product switch and a loading bar, and the system information page uses the new UI lib widgets for tiles and cards. Color scheme that follows the browser or can be forced from the theme settings.
1414 lines
44 KiB
Perl
Executable File
1414 lines
44 KiB
Perl
Executable File
# Virtualmin Framed Theme
|
|
# Icons copyright David Vignoni, all other theme elements copyright 2005-2007
|
|
# Virtualmin, Inc.
|
|
|
|
$main::cloudmin_no_create_links = 1;
|
|
$main::cloudmin_no_edit_buttons = 1;
|
|
$main::cloudmin_no_global_links = 1;
|
|
|
|
$main::mailbox_no_addressbook_button = 1;
|
|
$main::mailbox_no_folder_button = 1;
|
|
|
|
$main::basic_virtualmin_menu = 1;
|
|
$main::basic_virtualmin_domain = 1;
|
|
$main::nocreate_virtualmin_menu = 1;
|
|
$main::nosingledomain_virtualmin_mode = 1;
|
|
|
|
our $ui_formcount;
|
|
|
|
# Global state for wrapper
|
|
# if 0, wrapper isn't on, add one and open it, if 1 close it, if 2+, subtract
|
|
# but don't close
|
|
$main::WRAPPER_OPEN = 0;
|
|
$main::COLUMNS_WRAPPER_OPEN = 0;
|
|
|
|
# theme_ui_print_header(subtext, header-args...)
|
|
# Prints the page header like the core, but wraps the text under the
|
|
# title in a span of its own, then prints the subtext block
|
|
sub theme_ui_print_header
|
|
{
|
|
my ($text, @args) = @_;
|
|
if ($args[9] ne '') {
|
|
# Text under the title, such as a version, gets an element of its own
|
|
# so the stylesheet can make it smaller
|
|
$args[9] = "<span class='ui_header_below'>$args[9]</span>";
|
|
}
|
|
&header(@args);
|
|
print &ui_post_header($text);
|
|
}
|
|
|
|
# theme_ui_post_header([subtext])
|
|
# Returns HTML to appear directly after a standard header() call
|
|
sub theme_ui_post_header
|
|
{
|
|
my ($text) = @_;
|
|
my $rv;
|
|
$rv .= "<div class='ui_post_header'>$text</div>\n" if (defined($text));
|
|
$rv .= "<p></p>" if (!defined($text));
|
|
return $rv;
|
|
}
|
|
|
|
# theme_ui_pre_footer()
|
|
# Returns HTML to appear directly before a standard footer() call
|
|
sub theme_ui_pre_footer
|
|
{
|
|
my $rv;
|
|
$rv .= "<p></p>\n";
|
|
return $rv;
|
|
}
|
|
|
|
# ui_print_footer(args...)
|
|
# Print HTML for a footer with the pre-footer line. Args are the same as those
|
|
# passed to footer()
|
|
sub theme_ui_print_footer
|
|
{
|
|
local @args = @_;
|
|
print &ui_pre_footer();
|
|
if ($ui_formcount) {
|
|
print <<EOL;
|
|
<script>
|
|
(function(){
|
|
var forms = document.forms || [];
|
|
for(var i = 0; i < forms.length; i++){
|
|
for(var j = 0; j < forms[i].length; j++){
|
|
if(!forms[i][j].readonly != undefined && forms[i][j].type != "hidden" && forms[i][j].disabled != true && forms[i][j].style.display != 'none'){
|
|
forms[i][j].focus();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
})();
|
|
</script>
|
|
EOL
|
|
}
|
|
&footer(@args);
|
|
}
|
|
|
|
sub theme_icons_table
|
|
{
|
|
my ($i, $need_tr);
|
|
my $cols = $_[3] ? $_[3] : 4;
|
|
my $per = int(100.0 / $cols);
|
|
print "<div class='wrapper'>\n";
|
|
print "<table id='main' width=100% cellpadding=5 class='icons_table'>\n";
|
|
for($i=0; $i<@{$_[0]}; $i++) {
|
|
if ($i%$cols == 0) { print "<tr>\n"; }
|
|
print "<td width=$per% align=center valign=top>\n";
|
|
&generate_icon($_[2]->[$i], $_[1]->[$i], $_[0]->[$i],
|
|
$_[4], $_[5], $_[6], $_[7]->[$i], $_[8]->[$i]);
|
|
print "</td>\n";
|
|
if ($i%$cols == $cols-1) { print "</tr>\n"; }
|
|
}
|
|
while($i++%$cols) { print "<td width=$per%></td>\n"; $need_tr++; }
|
|
print "</tr>\n" if ($need_tr);
|
|
print "</table>\n";
|
|
print "</div>\n";
|
|
}
|
|
|
|
sub theme_generate_icon
|
|
{
|
|
my $w = !defined($_[4]) ? "width=48" : $_[4] ? "width=$_[4]" : "";
|
|
my $h = !defined($_[5]) ? "height=48" : $_[5] ? "height=$_[5]" : "";
|
|
if ($tconfig{'noicons'}) {
|
|
if ($_[2]) {
|
|
print "$_[6]<a href=\"$_[2]\" $_[3]>$_[1]</a>$_[7]\n";
|
|
}
|
|
else {
|
|
print "$_[6]$_[1]$_[7]\n";
|
|
}
|
|
}
|
|
elsif ($_[2]) {
|
|
print "<table><tr><td width=48 height=48>\n",
|
|
"<a href=\"$_[2]\" $_[3]><img src=\"$_[0]\" alt=\"\" border=0 ",
|
|
"$w $h></a></td></tr></table>\n";
|
|
print "$_[6]<a href=\"$_[2]\" $_[3]>$_[1]</a>$_[7]\n";
|
|
}
|
|
else {
|
|
print "<table><tr><td width=48 height=48>\n",
|
|
"<img src=\"$_[0]\" alt=\"\" border=0 $w $h>",
|
|
"</td></tr></table>\n$_[6]$_[1]$_[7]\n";
|
|
}
|
|
}
|
|
|
|
# theme_post_save_domain(&domain, action)
|
|
# Called by Virtualmin after a domain is updated, to refresh the left menu
|
|
sub theme_post_save_domain
|
|
{
|
|
local ($d, $action) = @_;
|
|
# Refresh left side, in case options have changed
|
|
print "<script>\n";
|
|
if ($action eq 'create') {
|
|
# Select the new domain
|
|
print "top.left.location = '@{[&get_webprefix()]}/left.cgi?mode=virtual-server&dom=$d->{'id'}';\n";
|
|
}
|
|
else {
|
|
# Just refresh left
|
|
print "top.left.location = top.left.location;\n";
|
|
}
|
|
print "</script>\n";
|
|
}
|
|
|
|
# theme_post_save_domains([domain, action]+)
|
|
# Called after multiple domains are updated, to refresh the left menu
|
|
sub theme_post_save_domains
|
|
{
|
|
print "<script>\n";
|
|
print "top.left.location = top.left.location;\n";
|
|
print "</script>\n";
|
|
}
|
|
|
|
# theme_post_save_server(&server, action)
|
|
# Called by Cloudmin after a server is updated, to refresh the left menu
|
|
sub theme_post_save_server
|
|
{
|
|
local ($s, $action) = @_;
|
|
if ($action eq 'create' || $action eq 'delete' ||
|
|
!$done_theme_post_save_server++) {
|
|
print "<script>\n";
|
|
print "top.left.location = top.left.location;\n";
|
|
print "</script>\n";
|
|
}
|
|
}
|
|
|
|
# theme_select_server(&server)
|
|
# Called by Cloudmin when a page for a server is displayed, to select it on the
|
|
# left menu.
|
|
sub theme_select_server
|
|
{
|
|
local ($server) = @_;
|
|
print <<EOF;
|
|
<script>
|
|
if (window.parent && window.parent.frames[0]) {
|
|
var leftdoc = window.parent.frames[0].document;
|
|
var leftform = leftdoc.forms[0];
|
|
if (leftform) {
|
|
var serversel = leftform['sid'];
|
|
if (serversel && serversel.value != '$server->{'id'}' ||
|
|
!serversel) {
|
|
//if (serversel) {
|
|
// // Need to change value of selector
|
|
// serversel.value = '$server->{'id'}';
|
|
// }
|
|
window.parent.frames[0].location = '@{[&get_webprefix()]}/left.cgi?mode=server-manager&sid=$server->{'id'}';
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
EOF
|
|
}
|
|
|
|
# theme_select_domain(&domain)
|
|
# Called by Virtualmin when a page for a server is displayed, to select it on
|
|
# the left menu.
|
|
sub theme_select_domain
|
|
{
|
|
local ($d) = @_;
|
|
print <<EOF;
|
|
<script>
|
|
if (window.parent && window.parent.frames[0]) {
|
|
var leftdoc = window.parent.frames[0].document;
|
|
var leftform = leftdoc.forms[0];
|
|
if (leftform) {
|
|
var domsel = leftform['dom'];
|
|
if (domsel && domsel.value != '$d->{'id'}') {
|
|
// Need to change value
|
|
// domsel.value = '$d->{'id'}';
|
|
window.parent.frames[0].location = '@{[&get_webprefix()]}/left.cgi?mode=virtual-server&dom=$d->{'id'}';
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
EOF
|
|
}
|
|
|
|
# theme_post_save_folder(&folder, action)
|
|
# Called after some folder is changed, to refresh the left frame. The action
|
|
# may be 'create', 'delete', 'modify' or 'read'
|
|
sub theme_post_save_folder
|
|
{
|
|
local ($folder, $action) = @_;
|
|
my $ref;
|
|
if ($action eq 'create' || $action eq 'delete' || $action eq 'modify') {
|
|
# Always refresh
|
|
$ref = 1;
|
|
}
|
|
else {
|
|
# Only refresh if showing unread count
|
|
if (defined(&mailbox::should_show_unread) &&
|
|
&mailbox::should_show_unread($folder)) {
|
|
$ref = 1;
|
|
}
|
|
}
|
|
if ($ref) {
|
|
print "<script>\n";
|
|
print "top.frames[0].document.location = top.frames[0].document.location;\n";
|
|
print "</script>\n";
|
|
}
|
|
}
|
|
|
|
sub theme_post_change_modules
|
|
{
|
|
print <<EOF;
|
|
<script>
|
|
var url = '' + top.left.location;
|
|
if (url.indexOf('mode=modules') > 0) {
|
|
top.left.location = url;
|
|
}
|
|
</script>
|
|
EOF
|
|
}
|
|
|
|
# theme_prebody(header-args...)
|
|
# Hides the module index link on Virtualmin pages, whose menu is already
|
|
# in the left frame
|
|
sub theme_prebody
|
|
{
|
|
if (get_module_name() eq "virtual-server") {
|
|
# No need for Module Index link, as we have the left-side frame
|
|
$tconfig{'nomoduleindex'} = 1;
|
|
}
|
|
}
|
|
|
|
# theme_prehead()
|
|
# Prints the head parts of every page: the color scheme meta, the scheme
|
|
# and frame scripts, and the links to the stylesheet and the table sorting
|
|
# script. Also sets the body attributes the stylesheet keys on: the module
|
|
# name as a class and the forced color scheme, if any.
|
|
sub theme_prehead
|
|
{
|
|
my $pfx = &get_webprefix();
|
|
my $scheme = &theme_color_scheme();
|
|
print "<meta name='color-scheme' content='".($scheme || "light dark")."'>\n";
|
|
print &theme_scheme_script() if (!$scheme);
|
|
print &theme_frame_script();
|
|
print "<link rel='stylesheet' type='text/css' href='$pfx/unauthenticated/".
|
|
"gray-theme.css?".&theme_asset_key("gray-theme.css")."'>\n";
|
|
print "<script type='text/javascript' src='$pfx/unauthenticated/".
|
|
"sorttable.js?".&theme_asset_key("sorttable.js")."'></script>\n";
|
|
my @cls = grep { $_ } ( $main::gray_theme_body_class, &get_module_name() );
|
|
$tconfig{'inbody'} = (@cls ? "class='".join(" ", @cls)."'" : "").
|
|
($scheme ? " data-scheme='$scheme'" : "");
|
|
}
|
|
|
|
# theme_scheme_script()
|
|
# Returns a script that keeps the color scheme steady when none is forced.
|
|
# A browser can report the system scheme for a moment before settling on
|
|
# its own, and gives a frame the scheme of the page holding it only once
|
|
# that page is styled, so a frame could paint dark and then turn light on
|
|
# every reload. The script applies the last scheme seen to the root
|
|
# element before the first paint and follows later changes. In a top
|
|
# window it also writes the scheme into the root style, so the frames
|
|
# inherit it, and checks again a second after load if no change came, in
|
|
# case the stored scheme was stale.
|
|
sub theme_scheme_script
|
|
{
|
|
return <<'EOF';
|
|
<script type='text/javascript'>
|
|
(function() {
|
|
var key = 'gray-theme-scheme', root = document.documentElement,
|
|
mq = window.matchMedia('(prefers-color-scheme: dark)'),
|
|
stored = null, changed = false;
|
|
try { stored = localStorage.getItem(key); } catch(e) { }
|
|
function current() { return mq.matches ? 'dark' : 'light'; }
|
|
function apply(v) {
|
|
root.setAttribute('data-scheme', v);
|
|
if (self == top) root.style.colorScheme = v;
|
|
try { localStorage.setItem(key, v); } catch(e) { }
|
|
}
|
|
apply(stored == 'dark' || stored == 'light' ? stored : current());
|
|
function onchange() { changed = true; apply(current()); }
|
|
if (mq.addEventListener) mq.addEventListener('change', onchange);
|
|
else mq.addListener(onchange);
|
|
if (self == top)
|
|
window.addEventListener('load', function() {
|
|
setTimeout(function() { if (!changed) apply(current()); }, 1000);
|
|
});
|
|
})();
|
|
</script>
|
|
EOF
|
|
}
|
|
|
|
# theme_frame_script()
|
|
# Returns a script for a page shown in the right frame, which tells the
|
|
# menu frame when the page starts to leave and when the next one has
|
|
# loaded, so the menu can show a loader in between. It does nothing in
|
|
# the menu frame itself, in popups and outside the frameset.
|
|
sub theme_frame_script
|
|
{
|
|
return <<'EOF';
|
|
<script type='text/javascript'>
|
|
(function() {
|
|
var menu = null;
|
|
try { if (parent != window) menu = parent.frames['left']; } catch(e) { }
|
|
if (!menu || menu == window) return;
|
|
function tell(name) { try { if (menu[name]) menu[name](); } catch(e) { } }
|
|
window.addEventListener('pagehide', function() { tell('rightLoading'); });
|
|
window.addEventListener('DOMContentLoaded', function() { tell('rightLoaded'); });
|
|
})();
|
|
</script>
|
|
EOF
|
|
}
|
|
|
|
# theme_color_scheme()
|
|
# Returns light or dark when the theme settings force a color scheme, or an
|
|
# empty string to follow the browser. The setting is stored with the other
|
|
# system information page settings, per user unless they are global.
|
|
sub theme_color_scheme
|
|
{
|
|
return $main::gray_theme_scheme if (defined($main::gray_theme_scheme));
|
|
my $file = "$config_directory/$current_theme/sections";
|
|
my %sects;
|
|
&read_file($file, \%sects);
|
|
if (!$sects{'global'}) {
|
|
# Unless the global settings are forced on everyone, the user's own
|
|
# file replaces them
|
|
my %usects;
|
|
%sects = %usects if (&read_file("$file.$remote_user", \%usects));
|
|
}
|
|
$main::gray_theme_scheme = $sects{'scheme'} =~ /^(light|dark)$/ ? $1 : "";
|
|
return $main::gray_theme_scheme;
|
|
}
|
|
|
|
# theme_asset_key(file)
|
|
# Returns the modification time of a file under unauthenticated, used as a
|
|
# cache key in its URL so browsers pick up a changed file at once
|
|
sub theme_asset_key
|
|
{
|
|
my ($file) = @_;
|
|
my @st = stat("$theme_root_directory/unauthenticated/$file");
|
|
return @st ? $st[9] : &get_webmin_version();
|
|
}
|
|
|
|
# theme_tag_class(tags, class)
|
|
# Adds a class to a string of tag attributes, merging it into an existing
|
|
# class attribute so that a tag never gets two of them
|
|
sub theme_tag_class
|
|
{
|
|
my ($tags, $class) = @_;
|
|
return "class='$class'" if (!$tags);
|
|
return $tags if ($tags =~ s/class=(['"])([^'"]*)\1/class=$1$2 $class$1/);
|
|
return "$tags class='$class'";
|
|
}
|
|
|
|
# theme_popup_prehead(title, ...)
|
|
# Popup windows and the frames get the same head as ordinary pages
|
|
sub theme_popup_prehead
|
|
{
|
|
return &theme_prehead();
|
|
}
|
|
|
|
# theme_ui_table_start(heading, [tabletags], [cols], [&default-tds],
|
|
# [right-heading])
|
|
# Returns HTML for the start of a form table: a card with the heading in
|
|
# its head row and the label and value rows in a nested table
|
|
sub theme_ui_table_start
|
|
{
|
|
my ($heading, $tabletags, $cols, $tds, $rightheading) = @_;
|
|
if (! $tabletags =~ /width/) { $tabletages .= " width=100%"; }
|
|
if (defined($main::ui_table_cols)) {
|
|
# Push on stack, for nested call
|
|
push(@main::ui_table_cols_stack, $main::ui_table_cols);
|
|
push(@main::ui_table_pos_stack, $main::ui_table_pos);
|
|
push(@main::ui_table_default_tds_stack, $main::ui_table_default_tds);
|
|
}
|
|
my $rv;
|
|
my $colspan = 1;
|
|
|
|
if (!$main::WRAPPER_OPEN) {
|
|
# A class in the table tags, such as the login form's, joins the
|
|
# wrapper class instead of being lost as a second class attribute
|
|
$rv .= "<table ".&theme_tag_class($tabletags, 'shrinkwrapper').">\n";
|
|
$rv .= "<tr><td>\n";
|
|
}
|
|
$main::WRAPPER_OPEN++;
|
|
$rv .= "<table class='ui_table' $tabletags>\n";
|
|
if (defined($heading) || defined($rightheading)) {
|
|
# Heading row, with an optional part at the right
|
|
$rv .= "<thead><tr>";
|
|
if (defined($heading)) {
|
|
$rv .= "<td><b>$heading</b></td>"
|
|
}
|
|
if (defined($rightheading)) {
|
|
$rv .= "<td align=right>$rightheading</td>";
|
|
$colspan++;
|
|
}
|
|
$rv .= "</tr></thead>\n";
|
|
}
|
|
$rv .= "<tbody> <tr class='ui_table_body'> <td colspan=$colspan>".
|
|
"<table width=100%>\n";
|
|
$main::ui_table_cols = $cols || 4;
|
|
$main::ui_table_pos = 0;
|
|
$main::ui_table_default_tds = $tds;
|
|
return $rv;
|
|
}
|
|
|
|
# ui_table_row(label, value, [cols], [&td-tags])
|
|
# Returns HTML for a row in a table started by ui_table_start, with a 1-column
|
|
# label and 1+ column value.
|
|
sub theme_ui_table_row
|
|
{
|
|
my ($label, $value, $cols, $tds, $trs) = @_;
|
|
$cols ||= 1;
|
|
$tds ||= $main::ui_table_default_tds;
|
|
my $rv;
|
|
if ($main::ui_table_pos+$cols+1 > $main::ui_table_cols &&
|
|
$main::ui_table_pos != 0) {
|
|
# If the requested number of cols won't fit in the number
|
|
# remaining, start a new row
|
|
my $leftover = $main::ui_table_cols - $main::ui_table_pos;
|
|
$rv .= "<td colspan=$leftover></td>\n";
|
|
$rv .= "</tr>\n";
|
|
$main::ui_table_pos = 0;
|
|
}
|
|
if (defined($label) &&
|
|
($value =~ /id="([^"]+)"/ || $value =~ /id='([^']+)'/ ||
|
|
$value =~ /id=([^>\s]+)/)) {
|
|
# Value contains an input with an ID
|
|
my $id = $1;
|
|
$label = "<label for=\""."e_escape($id)."\">$label</label>";
|
|
}
|
|
my $trtags_attrs = ref($trs) eq 'ARRAY' && $trs->[0] ? " $trs->[0]" : "";
|
|
my $trtags_class = ref($trs) eq 'ARRAY' && $trs->[1] ? " $trs->[1]" : "";
|
|
$rv .= "<tr class='ui_form_pair$trtags_class'$trtags_attrs>\n" if ($main::ui_table_pos%$main::ui_table_cols == 0);
|
|
$rv .= "<td class='ui_form_label' $tds->[0]><b>$label</b></td>\n" if (defined($label));
|
|
$rv .= "<td class='ui_form_value' colspan=$cols $tds->[1]>$value</td>\n";
|
|
$main::ui_table_pos += $cols+(defined($label) ? 1 : 0);
|
|
if ($main::ui_table_pos%$main::ui_table_cols == 0) {
|
|
$rv .= "</tr>\n";
|
|
$main::ui_table_pos = 0;
|
|
}
|
|
return $rv;
|
|
}
|
|
|
|
sub theme_ui_table_hr
|
|
{
|
|
my $rv;
|
|
if ($main::ui_table_pos) {
|
|
$rv .= "</tr>\n";
|
|
$main::ui_table_pos = 0;
|
|
}
|
|
$rv .= "<tr class='ui_form_pair'>\n";
|
|
$rv .= "<td class='ui_form_label' colspan=$main::ui_table_cols><hr></td>\n";
|
|
$rv .= "</tr>\n";
|
|
return $rv;
|
|
}
|
|
|
|
# theme_ui_table_end()
|
|
# Returns HTML for the end of a table started by theme_ui_table_start
|
|
sub theme_ui_table_end
|
|
{
|
|
my $rv;
|
|
if ($main::ui_table_cols == 4 && $main::ui_table_pos) {
|
|
# Add an empty block to balance the table
|
|
$rv .= &ui_table_row(" ", " ");
|
|
}
|
|
if (@main::ui_table_cols_stack) {
|
|
# Back to the column state of the enclosing table
|
|
$main::ui_table_cols = pop(@main::ui_table_cols_stack);
|
|
$main::ui_table_pos = pop(@main::ui_table_pos_stack);
|
|
$main::ui_table_default_tds = pop(@main::ui_table_default_tds_stack);
|
|
}
|
|
else {
|
|
# No enclosing table
|
|
$main::ui_table_cols = undef;
|
|
$main::ui_table_pos = undef;
|
|
$main::ui_table_default_tds = undef;
|
|
}
|
|
$rv .= "</tbody></table></td></tr></table>\n";
|
|
if ($main::WRAPPER_OPEN==1) {
|
|
# Close the card around the outermost table
|
|
$rv .= "</td></tr>\n";
|
|
$rv .= "</table>\n";
|
|
}
|
|
$main::WRAPPER_OPEN--;
|
|
return $rv;
|
|
}
|
|
|
|
# theme_ui_tabs_start(&tabs, name, selected, show-border)
|
|
# Render a row of tabs from which one can be selected. Each tab is an array
|
|
# ref containing a name, title and link. The tabs are links in a div, and
|
|
# select_tab switches their classes and the visible tab body.
|
|
sub theme_ui_tabs_start
|
|
{
|
|
my ($tabs, $name, $sel, $border) = @_;
|
|
my $rv;
|
|
if (!$main::ui_hidden_start_donejs++) {
|
|
# The tab switching script is printed once per page
|
|
$rv .= &ui_hidden_javascript();
|
|
}
|
|
|
|
# List of tab names, used by select_tab to find the tabs and bodies
|
|
my $tabnames = &convert_to_json([map { $_->[0] } @$tabs]);
|
|
$rv .= "<script>\n";
|
|
$rv .= "document.${name}_tabnames = $tabnames;\n";
|
|
$rv .= "</script>\n";
|
|
|
|
# Output the tabs
|
|
$rv .= &ui_hidden($name, $sel)."\n";
|
|
$rv .= "<div class='ui_tabs' id='tabs_$name'>\n";
|
|
foreach my $t (@$tabs) {
|
|
my $cls = $t->[0] eq $sel ? "ui_tab ui_tab_selected" : "ui_tab";
|
|
my $href = $t->[2] ne '' ? $t->[2] : '#';
|
|
$rv .= "<a id='tab_$t->[0]' class='$cls' href='$href' ".
|
|
"onClick='return select_tab(\"$name\", \"$t->[0]\")'>".
|
|
"$t->[1]</a>\n";
|
|
}
|
|
$rv .= "</div>\n";
|
|
|
|
if ($border) {
|
|
# All tab bodies are within a box
|
|
$rv .= "<div class='ui_tabs_box'>\n";
|
|
}
|
|
$main::ui_tabs_selected = $sel;
|
|
return $rv;
|
|
}
|
|
|
|
# theme_ui_tabs_end(show-border)
|
|
# Closes the box opened by theme_ui_tabs_start
|
|
sub theme_ui_tabs_end
|
|
{
|
|
my ($border) = @_;
|
|
return $border ? "</div>\n" : "";
|
|
}
|
|
|
|
# theme_ui_columns_start(&headings, [width-percent], [noborder], [&tdtags], [title])
|
|
# Returns HTML for a multi-column table, with the given headings
|
|
sub theme_ui_columns_start
|
|
{
|
|
my ($heads, $width, $noborder, $tdtags, $title, $sortable, $class) = @_;
|
|
my ($href) = grep { $_ =~ /<a\s+href/i } @$heads;
|
|
my $rv;
|
|
$theme_ui_columns_row_toggle = 0;
|
|
if (!$noborder && !$main::COLUMNS_WRAPPER_OPEN) {
|
|
# The outermost bordered table opens the card
|
|
$rv .= "<table class='wrapper' width="
|
|
. ($width ? $width : "100")
|
|
. "%>\n";
|
|
$rv .= "<tr><td>\n";
|
|
}
|
|
if (!$noborder) {
|
|
# Nested bordered tables share that card
|
|
$main::COLUMNS_WRAPPER_OPEN++;
|
|
}
|
|
# Tables are sorted by sorttable.js unless their headings are links, or
|
|
# always when the caller asked for a sortable table
|
|
my @classes;
|
|
push(@classes, "ui_table") if (!$noborder);
|
|
push(@classes, "sortable") if (!$href || $sortable);
|
|
push(@classes, "ui_columns");
|
|
push(@classes, $class) if ($class);
|
|
$rv .= "<table".(@classes ? " class='".join(" ", @classes)."'" : "").
|
|
(defined($width) ? " width=$width%" : "").
|
|
($sortable ? " data-sortable='1'" : "").">\n";
|
|
if ($title) {
|
|
# Title row spanning the table, above the column headings
|
|
$rv .= "<thead> <tr ".&theme_tag_class($tb, 'ui_columns_heading').">".
|
|
"<td colspan=".scalar(@$heads)."><b>$title</b></td>".
|
|
"</tr> </thead> <tbody>\n";
|
|
}
|
|
$rv .= "<thead> <tr ".&theme_tag_class($tb, 'ui_columns_heads').">\n";
|
|
my $i;
|
|
for($i=0; $i<@$heads; $i++) {
|
|
$rv .= "<td ".$tdtags->[$i]."><b>".
|
|
($heads->[$i] eq "" ? "<br>" : $heads->[$i])."</b></td>\n";
|
|
}
|
|
$rv .= "</tr></thead> <tbody>\n";
|
|
$theme_ui_columns_count++;
|
|
return $rv;
|
|
}
|
|
|
|
# theme_ui_columns_header(&columns, &tdtags)
|
|
# Returns HTML for a heading row inside a multi-column table
|
|
sub theme_ui_columns_header
|
|
{
|
|
my ($cols, $tdtags) = @_;
|
|
my $rv;
|
|
$rv .= "<tr ".&theme_tag_class($tb, 'ui_columns_header').">\n";
|
|
for(my $i=0; $i<@$cols; $i++) {
|
|
$rv .= "<td ".$tdtags->[$i]."><b>".
|
|
($cols->[$i] eq "" ? "<br>" : $cols->[$i])."</b></td>\n";
|
|
}
|
|
$rv .= "</tr>\n";
|
|
return $rv;
|
|
}
|
|
|
|
# theme_ui_columns_row(&columns, &tdtags)
|
|
# Returns HTML for a row in a multi-column table. Rows are highlighted on
|
|
# hover and when checked by the stylesheet, so no handlers are needed.
|
|
sub theme_ui_columns_row
|
|
{
|
|
$theme_ui_columns_row_toggle = $theme_ui_columns_row_toggle ? '0' : '1';
|
|
local ($cols, $tdtags) = @_;
|
|
my $rv;
|
|
$rv .= "<tr class='ui_columns_row row$theme_ui_columns_row_toggle'>\n";
|
|
my $i;
|
|
for($i=0; $i<@$cols; $i++) {
|
|
$rv .= "<td ".$tdtags->[$i].">".
|
|
($cols->[$i] !~ /\S/ ? "<br>" : $cols->[$i])."</td>\n";
|
|
}
|
|
$rv .= "</tr>\n";
|
|
return $rv;
|
|
}
|
|
|
|
# theme_ui_columns_end()
|
|
# Returns HTML to end a table started by ui_columns_start
|
|
sub theme_ui_columns_end
|
|
{
|
|
my $rv;
|
|
$rv = "</tbody> </table>\n";
|
|
if ($main::COLUMNS_WRAPPER_OPEN == 1) { # Last wrapper
|
|
$rv .= "</td> </tr> </table>\n";
|
|
}
|
|
$main::COLUMNS_WRAPPER_OPEN--;
|
|
return $rv;
|
|
}
|
|
|
|
# theme_ui_grid_table(&elements, columns, [width-percent], [tds], [tabletags],
|
|
# [title])
|
|
# Given a list of HTML elements, formats them into a table with the given
|
|
# number of columns. However, themes are free to override this to use fewer
|
|
# columns where space is limited.
|
|
sub theme_ui_grid_table
|
|
{
|
|
my ($elements, $cols, $width, $tds, $tabletags, $title) = @_;
|
|
return "" if (!@$elements);
|
|
|
|
my $rv = "<table class='wrapper' "
|
|
. ($width ? " width=$width%" : " width=100%")
|
|
. ($tabletags ? " ".$tabletags : "")
|
|
. "><tr><td>\n";
|
|
$rv .= "<table class='ui_table ui_grid_table'"
|
|
. ($width ? " width=$width%" : "")
|
|
. ($tabletags ? " ".$tabletags : "")
|
|
. ">\n";
|
|
if ($title) {
|
|
$rv .= "<thead><tr class='ui_grid_heading'> ".
|
|
"<td colspan=$cols><b>$title</b></td> </tr></thead>\n";
|
|
}
|
|
$rv .= "<tbody>\n";
|
|
my $i;
|
|
for($i=0; $i<@$elements; $i++) {
|
|
$rv .= "<tr class='ui_grid_row'>" if ($i%$cols == 0);
|
|
$rv .= "<td ".$tds->[$i%$cols]." valign=top class='ui_grid_cell'>".
|
|
$elements->[$i]."</td>\n";
|
|
$rv .= "</tr>" if ($i%$cols == $cols-1);
|
|
}
|
|
if ($i%$cols) {
|
|
while($i%$cols) {
|
|
$rv .= "<td ".$tds->[$i%$cols]." class='ui_grid_cell'><br></td>\n";
|
|
$i++;
|
|
}
|
|
$rv .= "</tr>\n";
|
|
}
|
|
$rv .= "</table>\n";
|
|
$rv .= "</tbody>\n";
|
|
$rv .= "</td></tr></table>\n"; # wrapper
|
|
return $rv;
|
|
}
|
|
|
|
sub theme_ui_hidden_start
|
|
{
|
|
my ($title, $name, $status) = @_;
|
|
my $rv;
|
|
my $opened = $status ? " open" : "";
|
|
$rv .= "<details class='ui_hidden_start'$opened>";
|
|
$rv .= "<summary>$title</summary>\n";
|
|
return $rv;
|
|
}
|
|
|
|
=head2 ui_hidden_end(name)
|
|
|
|
Returns HTML for the end of a hidden section, started by ui_hidden_start.
|
|
|
|
=cut
|
|
sub theme_ui_hidden_end
|
|
{
|
|
return "</details>\n";
|
|
}
|
|
|
|
# theme_ui_hidden_table_start(heading, [tabletags], [cols], name, status,
|
|
# [&default-tds], [rightheading])
|
|
# A table with a heading and table inside, and which is collapsible
|
|
sub theme_ui_hidden_table_start
|
|
{
|
|
my ($heading, $tabletags, $cols, $name, $status, $tds, $rightheading) = @_;
|
|
my $rv;
|
|
if (!$main::ui_hidden_start_donejs++) {
|
|
$rv .= &ui_hidden_javascript();
|
|
}
|
|
my $opened = $status ? " open" : "";
|
|
my $header = defined($heading) ? "<span>$heading</span>" : "";
|
|
my $rheader = defined($rightheading) ? "<span class='rightheading'>$rightheading</span>" : "";
|
|
if (!$main::WRAPPER_OPEN) { # If we're not already inside of a wrapper, wrap it
|
|
$rv .= "<div>\n";
|
|
}
|
|
$main::WRAPPER_OPEN++;
|
|
my $colspan = 1;
|
|
$rv .= "<details data-name='$name' class='ui_hidden_table_start'$opened $tabletags>";
|
|
$rv .= "<summary>$header $rheader</summary>\n";
|
|
$rv .= "<table width=100%>\n";
|
|
$main::ui_table_cols = $cols || 4;
|
|
$main::ui_table_pos = 0;
|
|
$main::ui_table_default_tds = $tds;
|
|
return $rv;
|
|
}
|
|
|
|
# ui_hidden_table_end(name)
|
|
# Returns HTML for the end of table with hiding, as started by
|
|
# ui_hidden_table_start
|
|
sub theme_ui_hidden_table_end
|
|
{
|
|
local $rv = "</table></details>\n";
|
|
if ( $main::WRAPPER_OPEN == 1 ) {
|
|
$main::WRAPPER_OPEN--;
|
|
$rv .= "</div>\n";
|
|
}
|
|
elsif ($main::WRAPPER_OPEN) { $main::WRAPPER_OPEN--; }
|
|
return $rv;
|
|
}
|
|
|
|
# theme_ui_checked_columns_row(&columns, &tdtags, checkname, checkvalue,
|
|
# [checked], [disabled], [tags])
|
|
# Returns HTML for a row with a checkbox in its first column. The row is
|
|
# highlighted by the stylesheet while its checkbox is checked.
|
|
sub theme_ui_checked_columns_row
|
|
{
|
|
$theme_ui_columns_row_toggle = $theme_ui_columns_row_toggle ? '0' : '1';
|
|
local ($cols, $tdtags, $checkname, $checkvalue, $checked, $disabled, $tags) = @_;
|
|
my $rv;
|
|
my $rid = "e_escape("row_${checkname}_${checkvalue}");
|
|
my $mycb = &theme_tag_class($cb,
|
|
"row$theme_ui_columns_row_toggle ui_checked_columns");
|
|
$rv .= "<tr id=\"$rid\" $mycb>\n";
|
|
$rv .= "<td class='ui_checked_checkbox' ".$tdtags->[0].">".
|
|
&ui_checkbox($checkname, $checkvalue, undef, $checked, $tags,
|
|
$disabled).
|
|
"</td>\n";
|
|
my $i;
|
|
for($i=0; $i<@$cols; $i++) {
|
|
$rv .= "<td ".$tdtags->[$i+1].">";
|
|
if ($cols->[$i] !~ /<a\s+href|<input|<select|<textarea/) {
|
|
# Plain cells become labels for the control, so a click on
|
|
# them selects it
|
|
$rv .= "<label for=\"".
|
|
"e_escape("${checkname}_${checkvalue}")."\">";
|
|
}
|
|
$rv .= ($cols->[$i] !~ /\S/ ? "<br>" : $cols->[$i]);
|
|
if ($cols->[$i] !~ /<a\s+href|<input|<select|<textarea/) {
|
|
# Close that label
|
|
$rv .= "</label>";
|
|
}
|
|
$rv .= "</td>\n";
|
|
}
|
|
$rv .= "</tr>\n";
|
|
return $rv;
|
|
}
|
|
|
|
# theme_ui_radio_columns_row(&columns, &tdtags, checkname, checkvalue,
|
|
# [checked])
|
|
# Returns HTML for a row with a radio button in its first column. The row
|
|
# is highlighted by the stylesheet while its button is selected.
|
|
sub theme_ui_radio_columns_row
|
|
{
|
|
local ($cols, $tdtags, $checkname, $checkvalue, $checked) = @_;
|
|
my $rv;
|
|
my $rid = "e_escape("row_${checkname}_${checkvalue}");
|
|
my $mycb = &theme_tag_class($cb, "ui_radio_columns");
|
|
$rv .= "<tr $mycb id=\"$rid\">\n";
|
|
$rv .= "<td ".$tdtags->[0]." class='ui_radio_radio'>".
|
|
&ui_oneradio($checkname, $checkvalue, undef, $checked).
|
|
"</td>\n";
|
|
my $i;
|
|
for($i=0; $i<@$cols; $i++) {
|
|
$rv .= "<td ".$tdtags->[$i+1].">";
|
|
if ($cols->[$i] !~ /<a\s+href|<input|<select|<textarea/) {
|
|
# Plain cells become labels for the control, so a click on
|
|
# them selects it
|
|
$rv .= "<label for=\"".
|
|
"e_escape("${checkname}_${checkvalue}")."\">";
|
|
}
|
|
$rv .= ($cols->[$i] !~ /\S/ ? "<br>" : $cols->[$i]);
|
|
if ($cols->[$i] !~ /<a\s+href|<input|<select|<textarea/) {
|
|
# Close that label
|
|
$rv .= "</label>";
|
|
}
|
|
$rv .= "</td>\n";
|
|
}
|
|
$rv .= "</tr>\n";
|
|
return $rv;
|
|
}
|
|
|
|
# theme_ui_nav_link(direction, url, disabled)
|
|
# Returns an arrow icon linking to provided url
|
|
sub theme_ui_nav_link
|
|
{
|
|
my ($direction, $url, $disabled) = @_;
|
|
my $icon = &ui_svg_icon($direction eq "left" ? "chevron-left"
|
|
: "chevron-right",
|
|
{ 'size' => 14,
|
|
'title' => $direction eq "left" ? '<-' : '->' });
|
|
if ($disabled) {
|
|
# A disabled arrow is plain, not a link
|
|
return "<span class='ui_nav_link ui_nav_link_disabled'>$icon</span>\n";
|
|
}
|
|
else {
|
|
# Arrow linking to the page
|
|
return "<a class='ui_nav_link' href=\"$url\">$icon</a>\n";
|
|
}
|
|
}
|
|
|
|
# theme_ui_links_row(&links)
|
|
# Returns a row of links separated by bars, in a block of its own so that
|
|
# the stylesheet can draw it as a toolbar when it sits above a table
|
|
sub theme_ui_links_row
|
|
{
|
|
my ($links) = @_;
|
|
return "" if (!$links || !@$links);
|
|
return "<div class='ui_links_row'>".
|
|
join(" <span class='ui_links_sep'>|</span> ", @$links).
|
|
"</div>\n";
|
|
}
|
|
|
|
# theme_file_chooser_button(input, type, [form], [chroot], [addmode])
|
|
# Returns a button that opens the file chooser, in a window large enough
|
|
# for the theme's controls unless the Webmin configuration sets a size
|
|
sub theme_file_chooser_button
|
|
{
|
|
my ($input, $type, $form, $chroot, $add) = @_;
|
|
$chroot = "/" if (!defined($chroot));
|
|
$add = int($add);
|
|
my ($w, $h) = (560, 500);
|
|
($w, $h) = split(/x/, $gconfig{'db_sizefile'}) if ($gconfig{'db_sizefile'});
|
|
return "<input type='button' class='ui_button ui_chooser_button' ".
|
|
"onClick='ifield = form.$input; chooser = window.open(\"".
|
|
&get_webprefix()."/chooser.cgi?add=$add&type=$type&chroot=$chroot".
|
|
"&file=\"+encodeURIComponent(ifield.value), \"chooser\", ".
|
|
"\"toolbar=no,menubar=no,scrollbars=no,resizable=yes,".
|
|
"width=$w,height=$h\"); chooser.ifield = ifield; ".
|
|
"window.ifield = ifield' value=\"...\">\n";
|
|
}
|
|
|
|
# theme_user_chooser_button(input, multiple, [form])
|
|
# Returns a button that opens the user chooser, sized like the file chooser
|
|
sub theme_user_chooser_button
|
|
{
|
|
my ($input, $multi) = @_;
|
|
return &theme_chooser_button("user_chooser.cgi", "user", $input, $multi);
|
|
}
|
|
|
|
# theme_group_chooser_button(input, multiple, [form])
|
|
# Returns a button that opens the group chooser, sized like the file chooser
|
|
sub theme_group_chooser_button
|
|
{
|
|
my ($input, $multi) = @_;
|
|
return &theme_chooser_button("group_chooser.cgi", "group", $input, $multi);
|
|
}
|
|
|
|
# theme_chooser_button(script, param, input, multiple)
|
|
# Returns the button shared by the user and group choosers. The multiple
|
|
# selection window has two panes and so is wider.
|
|
sub theme_chooser_button
|
|
{
|
|
my ($script, $param, $input, $multi) = @_;
|
|
my ($w, $h) = $multi ? (640, 500) : (420, 500);
|
|
if ($multi && $gconfig{'db_sizeusers'}) {
|
|
# Size of the multiple selection chooser from the Webmin config
|
|
($w, $h) = split(/x/, $gconfig{'db_sizeusers'});
|
|
}
|
|
elsif (!$multi && $gconfig{'db_sizeuser'}) {
|
|
# Size of the single selection chooser from the Webmin config
|
|
($w, $h) = split(/x/, $gconfig{'db_sizeuser'});
|
|
}
|
|
$multi = int($multi);
|
|
return "<button type='button' class='ui_button ui_chooser_button' ".
|
|
"onClick='ifield = form.$input; chooser = window.open(\"".
|
|
&get_webprefix()."/$script?multi=$multi&$param=\"+".
|
|
"escape(ifield.value), \"chooser\", ".
|
|
"\"toolbar=no,menubar=no,scrollbars=yes,resizable=yes,".
|
|
"width=$w,height=$h\"); chooser.ifield = ifield; ".
|
|
"window.ifield = ifield'>...</button>\n";
|
|
}
|
|
|
|
# theme_footer([page, name]+, [noendbody])
|
|
# Output a footer for returning to some page
|
|
sub theme_footer
|
|
{
|
|
my $i;
|
|
my @links;
|
|
my %module_info = get_module_info(get_module_name());
|
|
for($i=0; $i+1<@_; $i+=2) {
|
|
local $url = $_[$i];
|
|
if ($url ne '/' || !$tconfig{'noindex'}) {
|
|
# Turn the shortcut into a URL, unless the theme hides the
|
|
# Webmin index link
|
|
if ($url eq '/') {
|
|
# Webmin index, opened at the module's category
|
|
$url = "/?cat=$module_info{'category'}";
|
|
}
|
|
elsif ($url eq '' && get_module_name() eq 'virtual-server' ||
|
|
$url eq '/virtual-server/') {
|
|
# Don't bother with virtualmin menu
|
|
next;
|
|
}
|
|
elsif ($url eq '' && get_module_name() eq 'server-manager' ||
|
|
$url eq '/server-manager/') {
|
|
# Don't bother with Cloudmin menu
|
|
next;
|
|
}
|
|
#elsif ($url =~ /(view|edit)_domain.cgi/ &&
|
|
# get_module_name() eq 'virtual-server' ||
|
|
# $url =~ /^\/virtual-server\/(view|edit)_domain.cgi/) {
|
|
# # Don't bother with link to domain details
|
|
# next;
|
|
# }
|
|
elsif ($url =~ /edit_serv.cgi/ &&
|
|
get_module_name() eq 'server-manager' ||
|
|
$url =~ /^\/virtual-server\/edit_serv.cgi/) {
|
|
# Don't bother with link to system details
|
|
next;
|
|
}
|
|
elsif ($url eq '' && get_module_name()) {
|
|
# Index page of the current module
|
|
$url = "/".get_module_name()."/".
|
|
$module_info{'index_link'};
|
|
}
|
|
elsif ($url =~ /^\?/ && get_module_name()) {
|
|
# Index page of the module with parameters
|
|
$url = "/".get_module_name()."/$url";
|
|
}
|
|
$url = "@{[&get_webprefix()]}$url" if ($url =~ /^\//);
|
|
push(@links, "<a href=\"$url\">".
|
|
&text('main_return', $_[$i+1])."</a>");
|
|
}
|
|
}
|
|
if (@links) {
|
|
# Return links, with a chevron before the first
|
|
print "<div class='ui_footer_links'>\n";
|
|
print &ui_svg_icon("chevron-left", { 'size' => 14 }),"\n";
|
|
print join(" <span class='ui_footer_sep'>|</span>\n", @links),"\n";
|
|
print "</div>\n";
|
|
}
|
|
if (!$_[$i]) {
|
|
# End the page, unless the caller keeps the body open
|
|
my $postbody = $tconfig{'postbody'};
|
|
if ($postbody) {
|
|
# Text from the theme config at the end of every page, with
|
|
# its placeholders filled in
|
|
my $hostname = &get_display_hostname();
|
|
my $version = &get_webmin_version();
|
|
my $os_type = $gconfig{'real_os_type'} ||
|
|
$gconfig{'os_type'};
|
|
my $os_version = $gconfig{'real_os_version'} ||
|
|
$gconfig{'os_version'};
|
|
$postbody =~ s/%HOSTNAME%/$hostname/g;
|
|
$postbody =~ s/%VERSION%/$version/g;
|
|
$postbody =~ s/%USER%/$remote_user/g;
|
|
$postbody =~ s/%OS%/$os_type $os_version/g;
|
|
print "$postbody\n";
|
|
}
|
|
if ($tconfig{'postbodyinclude'}) {
|
|
# File from the theme directory at the end of every page
|
|
local $_;
|
|
open(INC, "$theme_root_directory/$tconfig{'postbodyinclude'}");
|
|
while(<INC>) {
|
|
print;
|
|
}
|
|
close(INC);
|
|
}
|
|
if (defined(&theme_postbody)) {
|
|
# Hook of an overlay theme
|
|
&theme_postbody(@_);
|
|
}
|
|
print "</body></html>\n";
|
|
}
|
|
}
|
|
|
|
# theme_redirect(original-url, url)
|
|
# Prints a redirect like the core, except that a Virtualmin page sending
|
|
# the browser to the server root goes to the framed system information
|
|
# page, since the Virtualmin menu is already in the left frame
|
|
sub theme_redirect
|
|
{
|
|
local ($orig, $url) = @_;
|
|
if (get_module_name() eq "virtual-server" && $orig eq "" &&
|
|
# Virtualmin's return to the root
|
|
$url =~ /^((http|https):\/\/([^\/]+))\//) {
|
|
$url = "$1/right.cgi";
|
|
}
|
|
print "Location: $url\n\n";
|
|
}
|
|
|
|
# theme_ui_hidden_javascript()
|
|
# Returns the script that switches tabs and opens hidden sections. Both
|
|
# only change classes; the stylesheet does the rest.
|
|
sub theme_ui_hidden_javascript
|
|
{
|
|
return <<EOF;
|
|
<script>
|
|
// Open or close a hidden section
|
|
function hidden_opener(divid, openerid)
|
|
{
|
|
var divobj = document.getElementById(divid);
|
|
var openerobj = document.getElementById(openerid);
|
|
var shown = divobj.className == 'opener_shown';
|
|
divobj.className = shown ? 'opener_hidden' : 'opener_shown';
|
|
if (openerobj) {
|
|
// Turn the arrow of the link that opens the section
|
|
openerobj.className = shown ? 'opener_closed' : 'opener_open';
|
|
}
|
|
}
|
|
|
|
// Show a tab and hide the others
|
|
function select_tab(name, tabname, form)
|
|
{
|
|
var tabnames = document[name+'_tabnames'];
|
|
for(var i=0; i<tabnames.length; i++) {
|
|
var tabobj = document.getElementById('tab_'+tabnames[i]);
|
|
var divobj = document.getElementById('div_'+tabnames[i]);
|
|
var selected = tabnames[i] == tabname;
|
|
if (tabobj) {
|
|
// Mark the tab itself
|
|
tabobj.className = selected ? 'ui_tab ui_tab_selected' : 'ui_tab';
|
|
}
|
|
if (!divobj) {
|
|
// A tab that is only a link has no body
|
|
continue;
|
|
}
|
|
divobj.className = (selected ? 'opener_shown' : 'opener_hidden')+
|
|
' ui_tabs_start';
|
|
if (selected) {
|
|
// Point the submit buttons of a nested form at it
|
|
try {
|
|
var nestedForm = divobj.querySelector("form[data-form-nested]");
|
|
if (nestedForm) {
|
|
var nestedFormId = nestedForm.getAttribute("data-form-nested"),
|
|
formSubmitters = document
|
|
.querySelectorAll(
|
|
"[data-submit-nested='" + nestedFormId + "']");
|
|
if (formSubmitters) {
|
|
formSubmitters.forEach(function(submitter) {
|
|
submitter.setAttribute(
|
|
"form", nestedForm.getAttribute('id'));
|
|
});
|
|
}
|
|
}
|
|
} catch(e) {
|
|
console.warn('Cannot set the related submitter ID of the nested form : ' + e);
|
|
}
|
|
}
|
|
}
|
|
if (document.forms[0] && document.forms[0][name]) {
|
|
// Remember the tab in the hidden field, so a submit returns to it
|
|
document.forms[0][name].value = tabname;
|
|
}
|
|
return false;
|
|
}
|
|
</script>
|
|
EOF
|
|
}
|
|
|
|
# Icons drawn before notices, by notice type and by the Font Awesome class
|
|
# names that modules pass to ui_alert
|
|
my %theme_alert_icons = (
|
|
'success' => 'check-circle',
|
|
'info' => 'info-circle',
|
|
'warning' => 'warning',
|
|
'danger' => 'x-circle',
|
|
'danger-fatal' => 'x-circle',
|
|
);
|
|
my %theme_fa_icons = (
|
|
'fa-check-circle' => 'check-circle',
|
|
'fa-check' => 'check',
|
|
'fa-info-circle' => 'info-circle',
|
|
'fa-question-circle' => 'question-circle',
|
|
'fa-exclamation-triangle' => 'warning',
|
|
'fa-warning' => 'warning',
|
|
'fa-bolt' => 'x-circle',
|
|
'fa-times-circle' => 'x-circle',
|
|
'fa-clock' => 'clock',
|
|
'fa-clock-o' => 'clock',
|
|
'fa-hdd-o' => 'hard-drive',
|
|
'fa-lock' => 'shield',
|
|
'fa-shield' => 'shield',
|
|
'fa-plug' => 'power',
|
|
'fa-power-off' => 'power',
|
|
'fa-download' => 'download',
|
|
'fa-upload' => 'upload',
|
|
'fa-refresh' => 'refresh',
|
|
'fa-search' => 'search',
|
|
'fa-server' => 'server',
|
|
'fa-user' => 'user',
|
|
'fa-trash' => 'trash',
|
|
'fa-cog' => 'gear',
|
|
'fa-globe' => 'globe',
|
|
'fa-star' => 'star',
|
|
'fa-play' => 'play',
|
|
'fa-stop' => 'stop',
|
|
'fa-terminal' => 'terminal',
|
|
'fa-book' => 'book',
|
|
'fa-filter' => 'filter',
|
|
'fa-pencil' => 'edit',
|
|
'fa-edit' => 'edit',
|
|
'fa-external-link' => 'external',
|
|
);
|
|
|
|
# theme_alert_icon(type, [fa-classes])
|
|
# Returns the icon shown before a notice, from a Font Awesome class name
|
|
# when the theme has an equivalent, else from the notice type
|
|
sub theme_alert_icon
|
|
{
|
|
my ($type, $fa) = @_;
|
|
my $name;
|
|
if ($fa) {
|
|
# Prefer the icon the module asked for, when the theme has it
|
|
my ($known) = grep { $theme_fa_icons{$_} } split(/\s+/, $fa);
|
|
$name = $theme_fa_icons{$known} if ($known);
|
|
}
|
|
$name ||= $theme_alert_icons{$type} || 'info-circle';
|
|
return "<span class='ui_alert_icon'>".
|
|
&ui_svg_icon($name, { 'size' => 18 })."</span>";
|
|
}
|
|
|
|
# theme_ui_alert_box(message, type, [style], [new-line], [title], [icon])
|
|
# Returns HTML for a notice box with an icon, taking the same arguments as
|
|
# the Authentic theme. The type can be success, info, warn, danger or
|
|
# danger-fatal, and picks the colors, the icon and the default title. The
|
|
# style is added to the box. With new-line set, the message starts under
|
|
# the title instead of after it. A title replaces the default one, and an
|
|
# empty title removes it. The icon is a Font Awesome class name, used when
|
|
# the theme has an equivalent. Buttons that the message puts on a line of
|
|
# their own, after a <p>, are wrapped together with the text before them,
|
|
# so both can share one line.
|
|
sub theme_ui_alert_box
|
|
{
|
|
my ($msg, $class, $style, $new_line, $desc_to_title, $desc_icon) = @_;
|
|
my %types = ( 'success' => [ 'success', $text{'ui_success'} ],
|
|
'info' => [ 'info', $text{'ui_info'} ],
|
|
'warn' => [ 'warning', $text{'ui_warning'} ],
|
|
'warning' => [ 'warning', $text{'ui_warning'} ],
|
|
'danger' => [ 'danger', $text{'ui_error'} ],
|
|
'danger-fatal' => [ 'danger-fatal', $text{'ui_error_fatal'} ] );
|
|
my ($type, $title) = @{$types{$class} || $types{'info'}};
|
|
# The default titles end in an exclamation mark, as in the Authentic theme
|
|
$title .= "!" if ($title ne '');
|
|
$title = $desc_to_title if (defined($desc_to_title));
|
|
my $lead = $title =~ /\S/ ?
|
|
"<strong>$title</strong>".($new_line ? "<br>" : " ") : "";
|
|
if ($msg =~ s/^(.*?)<p>\s*(<form\b.*?<\/form>)/<span class='ui_alert_text'>$lead$1<\/span><span class='ui_alert_actions'>$2<\/span>/is) {
|
|
# A form of buttons after the text
|
|
}
|
|
elsif ($msg =~ s/(<form\b[^>]*>)(.*?)<p>\s*((?:<input\b[^>]*>\s*)+)(<\/form>)/$1<span class='ui_alert_text'>$lead$2<\/span><span class='ui_alert_actions'>$3<\/span>$4/is) {
|
|
# Buttons after the text, inside its form
|
|
}
|
|
elsif ($msg =~ s/(<form\b[^>]*>)(.*?)<p>\s*((?:<input\b[^>]*>\s*)*<table\b[^>]*class=['"]ui_form_end_buttons['"].*?<\/table>)\s*(<\/form>)/$1<span class='ui_alert_text'>$lead$2<\/span><span class='ui_alert_actions'>$3<\/span>$4/is) {
|
|
# Buttons after the text, as a form end table inside its form
|
|
}
|
|
else {
|
|
# Plain message after the title
|
|
$msg = $lead.$msg;
|
|
}
|
|
return "<div class='ui_alert_box alert alert-$type'".
|
|
($style ? " style='"."e_escape($style)."'" : "").">".
|
|
&theme_alert_icon($type, $desc_icon).
|
|
"<div class='ui_alert_body'>$msg</div></div>\n";
|
|
}
|
|
|
|
# theme_ui_alert(content, [type], [icon], [&attrs])
|
|
# Returns HTML for an alert with an icon and a title, like the core
|
|
# ui_alert but drawn with the theme's SVG icons. The icon can be a Font
|
|
# Awesome class name, or an array of [ class, title, no-line-break ].
|
|
sub theme_ui_alert
|
|
{
|
|
my ($content, $type, $icon, $attrs) = @_;
|
|
$type ||= 'info';
|
|
my %titles = ( 'success' => $text{'ui_success'},
|
|
'info' => $text{'ui_info'},
|
|
'warning' => $text{'ui_warning'},
|
|
'danger' => $text{'ui_error'},
|
|
'danger-fatal' => $text{'ui_error_fatal'} );
|
|
my ($fa, $title, $br) = (undef, $titles{$type}, 1);
|
|
if (ref($icon)) {
|
|
# Icon given with its own title and line break flag
|
|
$fa = $icon->[0];
|
|
$title = $icon->[1] if (defined($icon->[1]));
|
|
$br = 0 if ($icon->[2]);
|
|
}
|
|
elsif (defined($icon)) {
|
|
# Icon name alone
|
|
$fa = $icon;
|
|
}
|
|
my %a = %{$attrs || {}};
|
|
$a{'class'} = join(" ", grep { $_ } ("alert", "alert-$type", $a{'class'}));
|
|
my $body = "";
|
|
$body .= "<strong>$title</strong>".($br ? "<br>" : " ") if ($title ne '');
|
|
$body .= "<span>$content</span>";
|
|
return &ui_tag_start('div', \%a).
|
|
&theme_alert_icon($type, $fa).
|
|
"<div class='ui_alert_body'>$body</div>".
|
|
&ui_tag_end('div')."\n";
|
|
}
|
|
|
|
# XXX Temporary until ui-lib.pl valign stuff gets cleaned up
|
|
#
|
|
# theme_ui_columns_table(&headings, width-percent, &data, &types, no-sort, title,
|
|
# empty-msg)
|
|
# Returns HTML for a complete table.
|
|
# headings - An array ref of heading HTML
|
|
# width-percent - Preferred total width
|
|
# data - A 2x2 array ref of table contents. Each can either be a simple string,
|
|
# or a hash ref like :
|
|
# { 'type' => 'group', 'desc' => 'Some section title' }
|
|
# { 'type' => 'string', 'value' => 'Foo', 'colums' => 3,
|
|
# 'nowrap' => 1 }
|
|
# { 'type' => 'checkbox', 'name' => 'd', 'value' => 'foo',
|
|
# 'label' => 'Yes', 'checked' => 1, 'disabled' => 1 }
|
|
# { 'type' => 'radio', 'name' => 'd', 'value' => 'foo', ... }
|
|
# types - An array ref of data types, such as 'string', 'number', 'bytes'
|
|
# or 'date'
|
|
# no-sort - Set to 1 to disable sorting by theme
|
|
# title - Text to appear above the table
|
|
# empty-msg - Message to display if no data
|
|
# sortable - Set to 1 to mark the table for client-side sorting
|
|
sub theme_ui_columns_table
|
|
{
|
|
my ($heads, $width, $data, $types, $nosort, $title, $emptymsg,
|
|
$sortable) = @_;
|
|
my $rv;
|
|
|
|
# Just show empty message if no data
|
|
if ($emptymsg && !@$data) {
|
|
$rv .= &ui_subheading($title) if ($title);
|
|
$rv .= "<b>$emptymsg</b><p>\n";
|
|
return $rv;
|
|
}
|
|
|
|
# Are there any checkboxes in each column? If so, make those columns narrow
|
|
my @tds;
|
|
my $maxwidth = 0;
|
|
foreach my $r (@$data) {
|
|
my $cc = 0;
|
|
foreach my $c (@$r) {
|
|
if (ref($c) &&
|
|
($c->{'type'} eq 'checkbox' || $c->{'type'} eq 'radio')) {
|
|
$tds[$cc] .= " width=5" if ($tds[$cc] !~ /width=/);
|
|
}
|
|
$cc++;
|
|
}
|
|
$maxwidth = $cc if ($cc > $maxwidth);
|
|
}
|
|
$rv .= &ui_columns_start($heads, $width, 0, \@tds, $title, $sortable);
|
|
|
|
# Add the data rows
|
|
foreach my $r (@$data) {
|
|
my $c0;
|
|
if (ref($r->[0]) && ($r->[0]->{'type'} eq 'checkbox' ||
|
|
$r->[0]->{'type'} eq 'radio')) {
|
|
# First column is special
|
|
$c0 = $r->[0];
|
|
$r = [ @$r[1..(@$r-1)] ];
|
|
}
|
|
# Turn data into HTML
|
|
my @rtds = @tds;
|
|
my @cols;
|
|
my $cn = 0;
|
|
$cn++ if ($c0);
|
|
foreach my $c (@$r) {
|
|
if (!ref($c)) {
|
|
# Plain old string
|
|
push(@cols, $c);
|
|
}
|
|
elsif ($c->{'type'} eq 'checkbox') {
|
|
# Checkbox in non-first column
|
|
push(@cols, &ui_checkbox($c->{'name'}, $c->{'value'},
|
|
$c->{'label'}, $c->{'checked'},
|
|
$c->{'tags'},
|
|
$c->{'disabled'}));
|
|
}
|
|
elsif ($c->{'type'} eq 'radio') {
|
|
# Radio button in non-first column
|
|
push(@cols, &ui_oneradio($c->{'name'}, $c->{'value'},
|
|
$c->{'label'}, $c->{'checked'},
|
|
$c->{'tags'},
|
|
$c->{'disabled'}));
|
|
}
|
|
elsif ($c->{'type'} eq 'group') {
|
|
# Header row that spans whole table
|
|
$rv .= &ui_columns_header([ $c->{'desc'} ],
|
|
[ "colspan=$width" ]);
|
|
next;
|
|
}
|
|
elsif ($c->{'type'} eq 'string') {
|
|
# A string, which might be special
|
|
push(@cols, $c->{'value'});
|
|
if ($c->{'columns'} > 1) {
|
|
splice(@rtds, $cn, $c->{'columns'},
|
|
"colspan=".$c->{'columns'});
|
|
}
|
|
if ($c->{'nowrap'}) {
|
|
$rtds[$cn] .= " nowrap";
|
|
}
|
|
}
|
|
$cn++;
|
|
}
|
|
# Add the row
|
|
if (!$c0) {
|
|
$rv .= &ui_columns_row(\@cols, \@rtds);
|
|
}
|
|
elsif ($c0->{'type'} eq 'checkbox') {
|
|
$rv .= &ui_checked_columns_row(\@cols, \@rtds, $c0->{'name'},
|
|
$c0->{'value'}, $c0->{'checked'},
|
|
$c0->{'disabled'},
|
|
$c0->{'tags'});
|
|
}
|
|
elsif ($c0->{'type'} eq 'radio') {
|
|
$rv .= &ui_radio_columns_row(\@cols, \@rtds, $c0->{'name'},
|
|
$c0->{'value'}, $c0->{'checked'},
|
|
$c0->{'disabled'},
|
|
$c0->{'tags'});
|
|
}
|
|
}
|
|
|
|
$rv .= &ui_columns_end();
|
|
return $rv;
|
|
}
|
|
|
|
=yui
|
|
|
|
Functions for generating YUI CSS grids markup.
|
|
|
|
=cut
|
|
|
|
# ui_yui_grid_start(id, type)
|
|
# Return a yui grid opening div.
|
|
# Available types are:
|
|
# g - 1/2,1/2
|
|
# gb - 1/3, 1/3, 1/3
|
|
# gc - 2/3, 1/3
|
|
# gd - 1/3, 2/3
|
|
# ge - 3/4, 1/4
|
|
# gf - 1/4, 3/4
|
|
sub theme_ui_yui_grid_start {
|
|
my ($id, $type) = @_;
|
|
return "<div id='grid_$id' class='yui-$type'>\n";
|
|
}
|
|
|
|
sub theme_ui_yui_grid_end {
|
|
my ($id) = @_;
|
|
return "</div> <!-- grid_$id -->\n";
|
|
}
|
|
|
|
# ui_yui_grid_section_start(id, first?)
|
|
# Return a yui grid markup section opening div.
|
|
sub theme_ui_yui_grid_section_start {
|
|
my ($id, $first) = @_;
|
|
if ($first) { return "<div id='grid_$id' class='yui-u first'>\n"; }
|
|
else { return "<div id='grid_$id' class='yui-u'>\n"; }
|
|
}
|
|
sub theme_ui_yui_grid_section_end {
|
|
my ($id) = @_;
|
|
return "</div> <!-- grid_$id -->\n";
|
|
}
|
|
|
|
1;
|
|
|