element or other object on the page, for instance a div created with
>>id=divname<<
text can be hidden/shown
>><<
Required parameter:
id=objname id attribute of object to be toggled
Optional parameters:
id2=obj2name second object (div), for toggling betwen first and second object
init=hide hides the element initially
(default: show)
show=labelname label of link or button when div is hidden
(default: "Show")
hide=labelname label of link or button when div is shown
(default: "Hide")
label=labelname label of link or button for both toggle states
(shortcut for setting 'show' and 'hide' to the same value)
ttshow=tooltiptext text that appears when the user hovers over the "show" link
(default: "Show")
tthide=tooltiptext text that appears when the user hovers over the "hide" link
(default: "Hide")
tt=tooltiptext text that appears when the user hovers over the link in both states
(shortcut for setting 'ttshow' and 'tthide' to the same value)
group=classname on clicking Show, show div with associated 'id' (standard behaviour),
but hide all other divs with class classname.
display=value what the 'display' CSS property of the specified element should
be set to, when it’s shown ('block', 'inline-block', etc.;
see https://www.w3schools.com/CSSref/pr_class_display.asp
for details)
(default: "block")
display2=value like 'display', but for the alternate element (id2)
(default: "block")
set=1 sets a cookie to remember toggle state
(default: 0)
button=1 display a button instead of a link
(default: 0)
printhidden=1 show hidden elements when printing
(default: 1)
nojs=integer set to 1 or 2 will show toggle links/buttons if browser
does not support javascript. Set to 2 will hide hidden
divs via style in page head and not via javascript,
so that for non-js browser initially hidden divs stay hidden.
(default: 0)
Alternative syntax: (:toggle divname:)
Alternative syntax with options:
(:toggle hide divname:) initial hide
(:toggle hide divname button:) initial hide, button
(:toggle name1 name2:) toggle between name1 and name2
See https://www.pmwiki.org/wiki/Cookbook/Toggle for additional info.
*/
# Recipe version (date).
$RecipeInfo['Toggle']['Version'] = '2025-01-18';
# Declare $Toggle for (:if enabled Toggle:) recipe installation check.
global $Toggle; $Toggle = 1;
# Defaults.
SDVA($ToggleConfig, [
'id' => '', // no default div name
'id2' => '', // no default div2 name
'init' => 'show', // initial state of element (visible)
'show' => XL("Show"), // link text ‘Show’
'hide' => XL("Hide"), // link text ‘Hide’
'ttshow' => XL("Show"), // tooltip text ‘Show’
'tthide' => XL("Hide"), // tooltip text ‘Hide’
'group' => '', // no default group (class) name
'display' => 'block', // default to `display:block`;
'display2' => '', // default alternate element to same as primary
'set' => false, // set no cookie to remember toggle state
'button' => false, // display as link by default
'printhidden' => true, // hidden divs get printed
'nojs' => 0, // in no-js browser links are not shown, initial hidden divs are shown
]);
$ToggleConfigStack = [ $ToggleConfig ];
# Retrieve cookie.
global $CookiePrefix, $pagename;
$current_page_group = PageVar($pagename, '$Group');
$current_page_name = PageVar($pagename, '$Name');
$toggle_cookie_name = "{$CookiePrefix}_toggle_{$current_page_group}_{$current_page_name}";
$toggle_cookie = isset($_COOKIE[$toggle_cookie_name])
? json_decode($_COOKIE[$toggle_cookie_name], true)
: null;
Markup('toggle', 'directives', '/\(:(toggle(?:set)?)\s+(.*?):\)/i', 'ToggleMarkup');
# All in one function.
function ToggleMarkup($m) {
global $HTMLHeaderFmt, $HTMLFooterFmt, $HTMLStylesFmt, $UploadUrlFmt, $UploadPrefixFmt;
global $ToggleConfigStack, $toggle_cookie_name, $toggle_cookie;
global $MarkupMarkupLevel;
extract($GLOBALS['MarkupToHTML']);
// Parse directive arguments.
$parsed_args = ParseArgs($m[2]);
// Get parameters without keys.
if ( isset($parsed_args[''])
&& is_array($parsed_args[''])) {
while (count($parsed_args['']) > 0) {
$parameter = array_shift($parsed_args['']);
if ($parameter == 'button')
$parsed_args['button'] = 1;
else if ($parameter == 'set')
$parsed_args['set'] = 1;
else if ($parameter == 'printhidden')
$parsed_args['printhidden'] = 1;
else if ($parameter == 'hide')
$parsed_args['init'] = 'hide';
else if ($parameter == 'show')
$parsed_args['init'] = 'show';
else if (!isset($parsed_args['id']))
$parsed_args['id'] = $parameter;
else if (!isset($parsed_args['id2']))
$parsed_args['id2'] = $parameter;
}
}
// Ensure that (:toggleset:) in (:markup:) only affects things on that
// and deeper markup levels.
for ($i = ($MarkupMarkupLevel ?? 0); !($ToggleConfig = ($ToggleConfigStack[$i] ?? null)); $i--);
if ($m[1] == 'toggleset') {
// Just setting options for toggles on the page.
$ToggleConfig = array_merge($ToggleConfig, $parsed_args);
unset($ToggleConfig['#']);
$ToggleConfigStack[($MarkupMarkupLevel ?? 0)] = $ToggleConfig;
return '';
} else {
// An actual toggle. Fill in un-specified parameters with defaults.
$opt = array_merge($ToggleConfig, $parsed_args);
}
$HTMLStylesFmt['toggle'] =
" @media print { .toggle { display: none; } } \n" .
".toggle img { border: none; } \n";
$HTMLHeaderFmt['toggle'] = <<<'EOT'
EOT;
# javascript for toggling and cookie setting
$HTMLFooterFmt['toggleobj'] = <<\n
EOT;
// Styling for errors.
$error_opening_tag = '';
$error_closing_tag = '';
// Retrieve the ids of both the primary and (if specified) alternate elements.
// (the 'div' options are for backwards compatibility with ShowHide and ToggleLink recipes)
$id = $opt['div'] ?? $opt['id'];
$id2 = $opt['div2'] ?? $opt['id2'];
if ($id == '') {
$error_message = '[Toggle] No object id specified.';
return Keep($error_opening_tag . $error_message . $error_closing_tag);
}
// Verify that the ids of both elements do not contain special characters
// which are forbidden in CSS identifiers. (Among other things, this forbids
// quotation marks, which prevents Javascript injection attacks via ids.)
$CSS_forbidden_characters_regex = '/[!"#\$%&\'\(\)\*\+,\.\/\:;<=>\?@\[\\\\\]\^`\{\|\}~]/';
$error_message_template = '[Toggle] Invalid ID specified for element: ELEMENT_ID';
$error_messages = [ ];
if (preg_match($CSS_forbidden_characters_regex, $id))
$error_messages[] = str_replace('ELEMENT_ID', $id, $error_message_template);
if (preg_match($CSS_forbidden_characters_regex, $id2))
$error_messages[] = str_replace('ELEMENT_ID', $id2, $error_message_template);
if (count($error_messages) > 0) {
return Keep(implode(' ', array_map(function ($msg) use ($error_opening_tag, $error_closing_tag) {
return ($error_opening_tag . $msg . $error_closing_tag);
}, $error_messages)));
}
// Values for the ‘display’ CSS property.
$display = $opt['display'];
$display2 = $opt['display2'] ?? $display;
// Set labels for (both states of) the toggle link/button.
$labels = [ ];
$labels['show'] = $opt['label'] ?? $opt['lshow'] ?? $opt['show'];
$labels['hide'] = $opt['label'] ?? $opt['lhide'] ?? $opt['hide'];
// Same with tooltips.
$tooltips = [ ];
$tooltips['show'] = $opt['tt'] ?? $opt['ttshow'];
$tooltips['hide'] = $opt['tt'] ?? $opt['tthide'] ;
// Transform label text into image tags, if appropriate.
// (since we won’t be putting the label text through pmwiki markup
// processing, which would normally process image attach links)
// (but maybe we should? TODO: investigate this)
// Also encode apostrophes (for non-images).
$ipat = '/^(.+\.(png|gif|jpg|jpeg|ico|svg))(?:\s*\|\s*(.+?))?$/i';
foreach ($labels as $k => $val) {
$is_image = preg_match($ipat, $val, $m);
if ($is_image) {
// Check for image, make image tag
$image = $m[1];
$prefix = (strstr($image, '/')) ? '/' : $UploadPrefixFmt;
$path = FmtPageName($UploadUrlFmt.$prefix, $pagename);
$tooltips[$k] = $m[3] ?? $tooltips[$k];
$labels[$k] = "";
$opt['button'] = '';
} else {
// Apostrophe encoding
$labels[$k] = str_replace("'", "’", $val);
}
}
// If the element is part of a defined group, then hide it, unless it’s explicitly set to be initially-shown.
if ( $opt['group'] != ''
&& ($parsed_args['init'] ?? '') != 'show')
$opt['init'] = 'hide';
// If set=1 (i.e. cookie setting enabled), then check if a cookie is set;
// if it is, read the element’s initial state from the cookie.
if ($opt['set'] == 1)
$opt['init'] = $toggle_cookie[$id] ?? $opt['init'];
/* OPTION RETRIEVAL ENDS; NOW PROCESSING BEGINS */
// Set initial state, and update labels and target state
// (for when user clicks the toggle/link button).
$display_property_value = ($opt['init'] == 'show') ? $display : 'none';
if (!($toggle_cookie[$id2] ?? null))
$alternate_element_display_property_value = ($opt['init'] == 'show') ? 'none' : $display2;
$label = ($opt['init'] == 'show') ? $labels['hide'] : $labels['show'];
$tooltip = ($opt['init'] == 'show') ? $tooltips['hide'] : $tooltips['show'];
$state_to_toggle_to = ($opt['init'] == 'show') ? 'hide' : 'show';
if ( $opt['nojs'] < 2
|| $opt['init'] == 'show') {
if (!isset($HTMLHeaderFmt['toggle-styles']))
$HTMLHeaderFmt['toggle-styles'] = '';
// Open script tag.
$HTMLHeaderFmt['toggle-styles'] .= '';
} else {
// Set initial state of element via embedded CSS.
$HTMLStylesFmt[] = "#{$id} { display: {$display_property_value}; } \n";
// Set initial state of alternate element (in the same way).
if ($id2)
$HTMLStylesFmt[] = " #{$id2} { display: {$alternate_element_display_property_value}; } \n";
}
// Set separate styles for print view, if ‘printhidden’ option is set.
if ($opt['printhidden'] == 1) {
$HTMLStylesFmt[] = "@media print { #{$id} { display: {$display} !important; } } \n";
if ($id2)
$HTMLStylesFmt[] = "@media print { #{$id2} { display: {$display} !important; } } \n";
}
// Save the Toggle state/data for this element.
$HTMLFooterFmt[] = <<
EOT;
// Construct toggle link or button (later it is modified with javascript).
$out = " 0 ? " no-js-visible" : "")
. "'>"
. ($opt['button'] == 1
? ""
: "{$label}")
. "";
return Keep($out);
}