xref: /dokuwiki/lib/scripts/page.js (revision 01a865f60621144a8a04cc76f48f11c31f135604)
1/**
2 * Page behaviours
3 *
4 * This class adds various behaviours to the rendered page
5 */
6dw_page = {
7    /**
8     * initialize page behaviours
9     */
10    init: function(){
11        dw_page.sectionHighlight();
12        dw_page.currentIDHighlight();
13        jQuery('a.fn_top').on('mouseover', dw_page.footnoteDisplay);
14        dw_page.makeToggle('#dw__toc h3','#dw__toc > div');
15    },
16
17    /**
18     * Highlight the section when hovering over the appropriate section edit button
19     *
20     * @author Andreas Gohr <andi@splitbrain.org>
21     */
22    sectionHighlight: function() {
23        jQuery('form.btn_secedit')
24            /*
25             * wrap the editable section in a div
26             */
27            .each(function () {
28                let $tgt = jQuery(this).parent();
29                const nr = $tgt.attr('class').match(/(\s+|^)editbutton_(\d+)(\s+|$)/)[2];
30                let $highlight = jQuery(); // holder for elements in the section to be highlighted
31                const $highlightWrap = jQuery('<div class="section_highlight_wrapper"></div>');
32
33                // the edit button should be part of the highlight
34                $highlight = $highlight.add($tgt);
35
36                // Walk the dom tree in reverse to find the sibling which is or contains the section edit marker
37                while ($tgt.length > 0 && !($tgt.hasClass('sectionedit' + nr) || $tgt.find('.sectionedit' + nr).length)) {
38                    $tgt = $tgt.prev();
39                    $highlight = $highlight.add($tgt);
40                }
41                // wrap the elements to be highlighted in the section highlight wrapper
42                $highlight.wrapAll($highlightWrap);
43            })
44            /*
45             * highlight the section
46             */
47            .on('mouseover', function () {
48                jQuery(this).parents('.section_highlight_wrapper').addClass('section_highlight');
49            })
50            /*
51             * remove highlight
52             */
53            .on('mouseout', function () {
54                jQuery(this).parents('.section_highlight_wrapper').removeClass('section_highlight');
55            });
56    },
57
58
59    /**
60     * Highlight internal link pointing to current page
61     *
62     * @author Henry Pan <dokuwiki@phy25.com>
63     */
64    currentIDHighlight: function(){
65        jQuery('a.wikilink1, a.wikilink2').filter('[data-wiki-id="'+JSINFO.id+'"]').wrap('<span class="curid"></div>');
66    },
67
68    /**
69     * Create/get a insitu popup used by the footnotes
70     *
71     * @param target - the DOM element at which the popup should be aligned at
72     * @param popup_id - the ID of the (new) DOM popup
73     * @return the Popup jQuery object
74     */
75    insituPopup: function(target, popup_id) {
76        // get or create the popup div
77        var $fndiv = jQuery('#' + popup_id);
78
79        // popup doesn't exist, yet -> create it
80        if($fndiv.length === 0){
81            $fndiv = jQuery(document.createElement('div'))
82                .attr('id', popup_id)
83                .addClass('insitu-footnote JSpopup')
84                .attr('aria-hidden', 'true')
85                .on('mouseleave', function () {jQuery(this).hide().attr('aria-hidden', 'true');})
86                .attr('role', 'tooltip');
87            jQuery('.dokuwiki:first').append($fndiv);
88        }
89
90        // position() does not support hidden elements
91        $fndiv.show().position({
92            my: 'left top',
93            at: 'left center',
94            of: target
95        }).hide();
96
97        return $fndiv;
98    },
99
100    /**
101     * Display an insitu footnote popup
102     *
103     * @author Andreas Gohr <andi@splitbrain.org>
104     * @author Chris Smith <chris@jalakai.co.uk>
105     * @author Anika Henke <anika@selfthinker.org>
106     */
107    footnoteDisplay: function () {
108        var $content = jQuery(jQuery(this).attr('href')) // Footnote text anchor
109                      .parent().siblings('.content').clone();
110
111        if (!$content.length) {
112            return;
113        }
114
115        // prefix ids on any elements with "insitu__" to ensure they remain unique
116        jQuery('[id]', $content).each(function(){
117            var id = jQuery(this).attr('id');
118            jQuery(this).attr('id', 'insitu__' + id);
119        });
120
121        var content = $content.html().trim();
122        // now put the content into the wrapper
123        dw_page.insituPopup(this, 'insitu__fn').html(content)
124        .show().attr('aria-hidden', 'false');
125    },
126
127    /**
128     * Makes an element foldable by clicking its handle
129     *
130     * This is used for the TOC toggling, but can be used for other elements
131     * as well. A state indicator is inserted into the handle and can be styled
132     * by CSS.
133     *
134     * To properly reserve space for the expanded element, the sliding animation is
135     * done on the children of the content. To make that look good and to make sure aria
136     * attributes are assigned correctly, it's recommended to make sure that the content
137     * element contains a single child element only.
138     *
139     * @param {selector} handle What should be clicked to toggle
140     * @param {selector} content This element will be toggled
141     * @param {int} state initial state (-1 = open, 1 = closed)
142     */
143    makeToggle: function(handle, content, state){
144        var $handle, $content, $clicky, $child, setClicky;
145        $handle = jQuery(handle);
146        if(!$handle.length) return;
147        $content = jQuery(content);
148        if(!$content.length) return;
149
150        // we animate the children
151        $child = $content.children();
152
153        // class/display toggling
154        setClicky = function(hiding){
155            if(hiding){
156                $clicky.html('<span>+</span>');
157                $handle.addClass('closed');
158                $handle.removeClass('open');
159            }else{
160                $clicky.html('<span>−</span>');
161                $handle.addClass('open');
162                $handle.removeClass('closed');
163            }
164        };
165
166        $handle[0].setState = function(state){
167            var hidden;
168            if(!state) state = 1;
169
170            // Assert that content instantly takes the whole space
171            $content.css('min-height', $content.height()).show();
172
173            // stop any running animation
174            $child.stop(true, true);
175
176            // was a state given or do we toggle?
177            if(state === -1) {
178                hidden = false;
179            } else if(state === 1) {
180                hidden = true;
181            } else {
182                hidden = $child.is(':hidden');
183            }
184
185            // update the state
186            setClicky(!hidden);
187
188            // Start animation and assure that $toc is hidden/visible
189            $child.dw_toggle(hidden, function () {
190                $content.toggle(hidden);
191                $content.attr('aria-expanded', hidden);
192                $content.css('min-height',''); // remove min-height again
193            }, true);
194        };
195
196        // the state indicator
197        $clicky = jQuery(document.createElement('strong'));
198
199        // click function
200        $handle.css('cursor','pointer')
201               .on('click', $handle[0].setState)
202               .prepend($clicky);
203
204        // initial state
205        $handle[0].setState(state);
206    }
207};
208
209jQuery(dw_page.init);
210