Bundled Templates :: Tabbed Panel

Disclaimer: This extension depends on client-side javascript and attaches JQuery, JQuery JSON and jStorage to documents that use it.

Reference

This extension renders a tabbed panel from document sections (hierarchical headlines in Doxia).

Tabbed panels are useful in improving the screen appearance of large content by creating multiple pages using tabs. When printing a document, the content renders in the same way as before (the panel is hidden and all tabs are visible).

Parameters:

Parameter Name Description
Input Parameters (set with the macro call)
"tabbed-panel" Enables this extension and sets a regular expression matching the section titles that should be transformed into a tabs.

Note: Setting this property has no effect in case of "source" or "source-class" were specified with the macro call. This behaviour ensures that the parameter "tabbed-panel" can still be used with ordinary macro calls where a source or source-class was set.

This parameter expects a regular expression that is used to select what sections turn into tabs and what not by matching the given titles (headlines).
By default the javascript looks for the first section that follows the include position and continues with all siblings (sections of the same level). Neither parent nor child sections are processed, therefore it's mostly safe to use the match all expression .* if all sub-sequent sections of the same nesting level should turn into tabs.

Examples:
  • "%{include|tabbed-panel=.*}" - Transforms all following sections (of the same level) to tabs.
  • "%{include|tabbed-panel=.*(Example).*}" - Transforms all following sections to tabs that contain the word "Example".
"hide-titles" Toggles whether section titles are hidden once they were transformed into a tab (defaults to 'true').
"min-height" Optional Parameter: Specifies a minimum tab height to improve scrolling behaviour when switching tabs.
Implementation "org.tinyjee.maven.dim.extensions.TabbedPanelParameterTransformer": ApiDoc | Source

Usage & Template Source

%{include|tabbed-panel=.*}

Example

Some section content, above another nested panel:

Sub Section 1

SubSection content A.

Sub Section 2

SubSection content B.

Sub Section 3

SubSection content C.

Page Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
    ------
    Bundled Templates :: Tabbed Panel
    ------
    Juergen Kellerer
    ------
    2011-11-06
    ------
  
Bundled Templates :: Tabbed Panel
  
%{include|source=disclaimers.xdoc.vm|clientSideJavaScript=JQuery, JQuery JSON and jStorage}
  
* Reference
  
%{include|source=addon-interface-table.xdoc.vm|source-java=org.tinyjee.maven.dim.extensions.TabbedPanelParameterTransformer}
  
* Usage & Template Source
  
----------------
%{include|tabbed-panel=.*}
----------------
  
%{include|tabbed-panel=.*}
  
* Example
  
    Some section content, above another nested panel:
  
%{include|tabbed-panel=.*|hide-titles=false|min-height=200px}
  
** Sub Section 1
  
    SubSection content A.
  
** Sub Section 2
  
    SubSection content B.
  
** Sub Section 3
  
    SubSection content C.
  
* Page Source
  
%{include|source=apt/templatesTabbedPanel.apt|source-content-type=text}
  
* Template Sources
  
%{include|tabbed-panel=.*}
  
** Velocity Template
  
%{include|source=classpath:/templates/dim/tabbed-panel.html.vm|source-is-template=false}
  
** JS
  
%{include|tabbed-panel=.*}
  
*** tabbed-panel.js
  
%{include|source=classpath:/templates/dim/js/tabbed-panel.js}
  
*** section-finder.js
  
%{include|source=classpath:/templates/dim/js/section-finder.js}
  
** CSS
  
%{include|source=classpath:/templates/dim/css/tabbed-panel.css}

Template Sources

Velocity Template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#*
    Implements a tabbed panel using JQuery and bundled style-sheets and images.
    When used, the javascript code looks after section titles that match the given regular expression and
    transforms them to tabs.
  
    The lookup strategy for the first section title is:
    - Find a div of class "section" below the DOM tree whose section title matches the regular expression.
    - If no title matched, traverse up the DOM tree and find a section title that matches the regular expression.
  
    Once the first title is found, all matching siblings are transformed to tabs as well.
  
    Notes:
    - When printing the document, the tabbed panel is hidden and all tags are shown.
    - The tabbed panel is not created at all when used with a non HTML sink.
  
    User suppliable parameters:
  
    "tabs"         RegExp    A regular expression that is used to select the section titles to include.
    "hide-titles"  Boolean   Toggles whether section titles are hidden when transformed into tabs.
*#
#dim_attachJs(["jquery", "jquery.json.min", "jstorage.min", "dim/js/section-finder", "dim/js/tabbed-panel"], $jsSuccess)
#dim_attachCss(["dim/css/tabbed-panel"], $cssSuccess)
  
#if($jsSuccess && $cssSuccess)
    #dim_attachBinary("dim/images/tabbed-panel-top-border.png", $topBorderImage)
    #dim_attachBinary("dim/images/tabbed-panel-background.png", $backgroundImage)
  
    #dim_attachBinary("dim/images/tabbed-panel-tab-background.png", $tabBackgroundImage)
    #dim_attachBinary("dim/images/tabbed-panel-tab-left-corner.png", $leftCornerImage)
    #dim_attachBinary("dim/images/tabbed-panel-tab-right-corner.png", $rightCornerImage)
  
    #if (!$tabs || $tabs == "*") #set($tabs = ".*") #end
    #if (!$hide-titles || $hide-titles == "false")
        #set($hide-titles = "false")
    #else
        #set($hide-titles = "true")
    #end
    #if (!$min-height) #set($min-height = "") #end
  
    #set($panelId = "tabbed-panel-" + $globals.nextLocalId())
  
<div class="tabbed-panel" id="$panelId">
    <div class="tabbed-panel-top-border"></div>
    <div class="tabbed-panel-table">
        <table border="0" cellpadding="0" cellspacing="0">
            <tr valign="middle"></tr>
        </table>
    </div>
    <script type="text/javascript" language="javascript">
        $(function() {
            jsDim.newTabbedPanel($("#$panelId"), function(tabTitle) {
                return /${tabs}/gi.test(tabTitle);
            }, {
                leftCornerImage: "$leftCornerImage",
                rightCornerImage: "$rightCornerImage",
                hideTitles: $hide-titles,
                minHeight: "$min-height"
            });
        });
    </script>
</div>
#end

JS

tabbed-panel.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/*
 * Copyright 2011 - Doxia :: Include Macro - Juergen Kellerer
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
  
/**
 * Creates the method "window.jsDim.newTabbedPanel(panelId, selectTitleCallback, configuration)"
 */
(function(window) {
    /**
     * Creates a new tabbed panel.
     *
     * @param panelContainer The panel div to create the tabbed panel in.
     * @param selectTitleCallback A callback of the format <code>function(title) { return true/false; }</code>
     * @param config A object of the format: <code>{leftCornerImage: "",
     *                      rightCornetImage: "", minHeight: "", hideTitles: true/false}</code>
     */
    function newTabbedPanel(panelContainer, selectTitleCallback, config) {
        var tabbedPanel = $(panelContainer);
        var tableRow = tabbedPanel.find("tr").first();
        var tabIndexStorageKey = createTabIndexStorageKey(tabbedPanel.attr("id"));
  
        var indexedTabs = [], indexedTabContents = jsDim.protectSections(jsDim.findSections(tabbedPanel, selectTitleCallback));
        $.each(indexedTabContents, function(index, section) {
            var tabTitleElement = $(section).find("h1,h2,h3,h4,h5,h6").first();
            var tabTitle = tabTitleElement.text();
            if (config.hideTitles) tabTitleElement.addClass("inactive");
            if (config.minHeight != "")
                $(section).css("minHeight", /^[0-9]+$/.test(config.minHeight) ? config.minHeight + "px" : config.minHeight);
  
            indexedTabs[index] = $("<td class='inactive'></td>").text(tabTitle).data("index", index).click(function() {
                $("td.active", tableRow).addClass("inactive").removeClass("active");
                $("td.corner", tableRow).hide();
                $(this).addClass("active").removeClass("inactive");
                $(this).prev("td").show();
                $(this).next("td").show();
  
                var selectedTabIndex = $(this).data("index");
                tabbedPanel.data("selectedTabIndex", selectedTabIndex);
                $.jStorage.set(tabIndexStorageKey, {index: selectedTabIndex, time: new Date().getTime()});
                for (var i = 0; i < indexedTabContents.length; i++) {
                    var active = i == selectedTabIndex;
                    indexedTabContents[i].addClass(active ? "active" : "inactive").removeClass(active ? "inactive" : "active");
                }
            });
  
            $(section).addClass("inactive");
  
            function cornerImageBuilder(src) {
                return "<td class='corner' style='display:none;'><img src='" + src + "' width='15' height='28'/></td>";
            }
  
            tableRow.append(cornerImageBuilder(config.leftCornerImage));
            tableRow.append(indexedTabs[index]);
            tableRow.append(cornerImageBuilder(config.rightCornerImage));
        });
  
        // Add data to the tabbed-panel (div) to allow interaction via javascript.
        tabbedPanel.data("selectedTabIndex", -1);
        tabbedPanel.data("indexedTabs", indexedTabs);
        tabbedPanel.data("indexedTabContents", indexedTabContents);
  
        // Select one tab
        if (indexedTabs.length > 0) {
            clearOutdatedTabIndexStorageKeys();
            var selectedTabIndex = Math.min($.jStorage.get(tabIndexStorageKey, {index: 0}).index, indexedTabs.length);
            indexedTabs[isNaN(selectedTabIndex) ? 0 : selectedTabIndex].click();
        }
    }
  
    // Max age of a stored tab index key
    var maxStoredTabIndexAge = 60 * 60 * 1000;
  
    function createTabIndexStorageKey(panelId) {
        return "tab-index-" + document.location.href + "#" + panelId;
    }
  
    function clearOutdatedTabIndexStorageKeys() {
        var referenceTime = new Date().getTime();
        $.each($.jStorage.index(), function(index, key) {
            if (/^tab-index-.*/.test(key)) {
                var value = $.jStorage.get(key);
                if (value && value.time && referenceTime - value.time > maxStoredTabIndexAge)
                    $.jStorage.deleteKey(key);
            }
        });
    }
  
    // Register in "window.jsDim"
    if (!window.jsDim) window.jsDim = {};
    window.jsDim.newTabbedPanel = newTabbedPanel;
})(window);
section-finder.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/*
 * Copyright 2011 - Doxia :: Include Macro - Juergen Kellerer
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
  
  
/**
 * Creates the method "window.jsDim.findSections(jQuery, selectSectionTitleCallback)"
 */
(function(window) {
    /**
     * Creates a new tabbed panel.
     *
     * @param startElement An arbitrary element to start with the search for sections.
     * @param selectSectionTitleCallback A callback of the format <code>function(title) { return true/false; }</code>
     * @return An array of jQuery objects containing the matching sections.
     */
    function findSections(startElement, selectSectionTitleCallback) {
        var theSectionWeAreIn = $(startElement).closest("div.section");
  
        var indexedSections = [], navigationType = 0, sections;
        while (indexedSections.length == 0) {
            switch (navigationType++) {
                case 0:
                    // Find the first section below this and select siblings.
                    sections = theSectionWeAreIn.find("div.section").not(".protected").first().nextAll("div.section").andSelf();
                    break;
                case 1:
                    // Looking for the section we're currently in and select siblings.
                    sections = theSectionWeAreIn.nextAll("div.section").not(".protected");
                    break;
                default:
                    return [];
            }
  
            var index = 0;
            sections.each(function() {
                var tabTitle = $(this).find("h1,h2,h3,h4,h5,h6").first().text();
                if (selectSectionTitleCallback(tabTitle))
                    indexedSections[index++] = $(this);
            });
        }
  
        return indexedSections;
    }
  
    /**
     * Protects the given sections from being selected again.
     * @param sections the sections to protect.
     * @return the given list of sections after protecting them.
     */
    function protectSections(sections) {
        $.each(sections, function(index, section) {
            $(section).addClass("protected");
        });
        return sections;
    }
  
    // Register in "window.jsDim"
    if (!window.jsDim) window.jsDim = {};
    window.jsDim.findSections = findSections;
    window.jsDim.protectSections = protectSections;
})(window);

CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/*
 * Copyright 2011 - Doxia :: Include Macro - Juergen Kellerer
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
  
div.tabbed-panel, .tabbed-panel div, .tabbed-panel img, .tabbed-panel td, .tabbed-panel tr, .tabbed-panel table {
    border: 0;
    margin: 0;
    padding: 0;
}
  
div.tabbed-panel {
    margin-top: 8pt;
    margin-bottom: 4pt;
}
  
div.tabbed-panel-top-border {
    background-image: url("../images/tabbed-panel-top-border.png");
    background-repeat: repeat-x;
    height: 10px;
}
  
div.tabbed-panel-table, .tabbed-panel-table table {
    background-image: url("../images/tabbed-panel-background.png");
    background-repeat: repeat-x;
    height: 28px;
}
  
.tabbed-panel-table table {
    width: auto;
}
  
.tabbed-panel-table td {
    font-size: 11px;
    line-height: 25px;
    text-align: center;
    white-space: nowrap;
    cursor: pointer;
}
  
.tabbed-panel-table td.active {
    background-image: url("../images/tabbed-panel-tab-background.png");
    background-repeat: repeat-x;
    padding-left: 2px;
    padding-right: 2px;
    font-weight: bold;
}
  
.tabbed-panel-table td.inactive {
    padding-left: 17px;
    padding-right: 17px;
}
  
div.section div.active {
    display: block;
}
  
div.section div.inactive, h1.inactive, h2.inactive, h3.inactive, h4.inactive, h5.inactive, h6.inactive {
    display: none;
}
  
/* hide the tabbed panel when printing the page */
@media print {
    div.tabbed-panel {
        display: none;
    }
    div.section div.inactive, h1.inactive, h2.inactive, h3.inactive, h4.inactive, h5.inactive, h6.inactive {
        display: block;
    }
}