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 dw_page.copyCode(); 16 }, 17 18 /** 19 * Highlight the section when hovering over the appropriate section edit button 20 * 21 * @author Andreas Gohr <andi@splitbrain.org> 22 */ 23 sectionHighlight: function() { 24 jQuery('form.btn_secedit') 25 /* 26 * wrap the editable section in a div 27 */ 28 .each(function () { 29 let $tgt = jQuery(this).parent(); 30 const match = $tgt.attr('class').match(/(\s+|^)editbutton_(\d+)(\s+|$)/); 31 if(!match) return; 32 const nr = match[2]; 33 let $highlight = jQuery(); // holder for elements in the section to be highlighted 34 const $highlightWrap = jQuery('<div class="section_highlight_wrapper"></div>'); 35 36 // the edit button should be part of the highlight 37 $highlight = $highlight.add($tgt); 38 39 // Walk the dom tree in reverse to find the sibling which is or contains the section edit marker 40 while ($tgt.length > 0 && !($tgt.hasClass('sectionedit' + nr) || $tgt.find('.sectionedit' + nr).length)) { 41 $tgt = $tgt.prev(); 42 $highlight = $highlight.add($tgt); 43 } 44 // wrap the elements to be highlighted in the section highlight wrapper 45 $highlight.wrapAll($highlightWrap); 46 }) 47 /* 48 * highlight the section 49 */ 50 .on('mouseover', function () { 51 jQuery(this).parents('.section_highlight_wrapper').addClass('section_highlight'); 52 }) 53 /* 54 * remove highlight 55 */ 56 .on('mouseout', function () { 57 jQuery(this).parents('.section_highlight_wrapper').removeClass('section_highlight'); 58 }); 59 }, 60 61 62 /** 63 * Highlight internal link pointing to current page 64 * 65 * @author Henry Pan <dokuwiki@phy25.com> 66 */ 67 currentIDHighlight: function(){ 68 jQuery('a.wikilink1, a.wikilink2').filter('[data-wiki-id="'+JSINFO.id+'"]').wrap('<span class="curid"></span>'); 69 }, 70 71 /** 72 * Create/get a insitu popup used by the footnotes 73 * 74 * @param target - the DOM element at which the popup should be aligned at 75 * @param popup_id - the ID of the (new) DOM popup 76 * @return the Popup jQuery object 77 */ 78 insituPopup: function(target, popup_id) { 79 // get or create the popup div 80 var $fndiv = jQuery('#' + popup_id); 81 82 // popup doesn't exist, yet -> create it 83 if($fndiv.length === 0){ 84 $fndiv = jQuery(document.createElement('div')) 85 .attr('id', popup_id) 86 .addClass('insitu-footnote JSpopup') 87 .attr('aria-hidden', 'true') 88 .on('mouseleave', function () {jQuery(this).hide().attr('aria-hidden', 'true');}) 89 .attr('role', 'tooltip'); 90 jQuery('.dokuwiki:first').append($fndiv); 91 } 92 93 // position() does not support hidden elements 94 $fndiv.show().position({ 95 my: 'left top', 96 at: 'left center', 97 of: target 98 }).hide(); 99 100 return $fndiv; 101 }, 102 103 /** 104 * Display an insitu footnote popup 105 * 106 * @author Andreas Gohr <andi@splitbrain.org> 107 * @author Chris Smith <chris@jalakai.co.uk> 108 * @author Anika Henke <anika@selfthinker.org> 109 */ 110 footnoteDisplay: function () { 111 var $content = jQuery(jQuery(this).attr('href')) // Footnote text anchor 112 .parent().siblings('.content').clone(); 113 114 if (!$content.length) { 115 return; 116 } 117 118 // prefix ids on any elements with "insitu__" to ensure they remain unique 119 jQuery('[id]', $content).each(function(){ 120 var id = jQuery(this).attr('id'); 121 jQuery(this).attr('id', 'insitu__' + id); 122 }); 123 124 var content = $content.html().trim(); 125 // now put the content into the wrapper 126 dw_page.insituPopup(this, 'insitu__fn').html(content) 127 .show().attr('aria-hidden', 'false'); 128 }, 129 130 /** 131 * Makes an element foldable by clicking its handle 132 * 133 * This is used for the TOC toggling, but can be used for other elements 134 * as well. A state indicator is inserted into the handle and can be styled 135 * by CSS. 136 * 137 * To properly reserve space for the expanded element, the sliding animation is 138 * done on the children of the content. To make that look good and to make sure aria 139 * attributes are assigned correctly, it's recommended to make sure that the content 140 * element contains a single child element only. 141 * 142 * @param {selector} handle What should be clicked to toggle 143 * @param {selector} content This element will be toggled 144 * @param {int} state initial state (-1 = open, 1 = closed) 145 */ 146 makeToggle: function(handle, content, state){ 147 var $handle, $content, $clicky, $child, setClicky; 148 $handle = jQuery(handle); 149 if(!$handle.length) return; 150 $content = jQuery(content); 151 if(!$content.length) return; 152 153 // we animate the children 154 $child = $content.children(); 155 156 // class/display toggling 157 setClicky = function(hiding){ 158 if(hiding){ 159 $clicky.html('<span>+</span>'); 160 $handle.addClass('closed'); 161 $handle.removeClass('open'); 162 }else{ 163 $clicky.html('<span>−</span>'); 164 $handle.addClass('open'); 165 $handle.removeClass('closed'); 166 } 167 }; 168 169 $handle[0].setState = function(state){ 170 var hidden; 171 if(!state) state = 1; 172 173 // Assert that content instantly takes the whole space 174 $content.css('min-height', $content.height()).show(); 175 176 // stop any running animation 177 $child.stop(true, true); 178 179 // was a state given or do we toggle? 180 if(state === -1) { 181 hidden = false; 182 } else if(state === 1) { 183 hidden = true; 184 } else { 185 hidden = $child.is(':hidden'); 186 } 187 188 // update the state 189 setClicky(!hidden); 190 191 // Start animation and assure that $toc is hidden/visible 192 $child.dw_toggle(hidden, function () { 193 $content.toggle(hidden); 194 $content.attr('aria-expanded', hidden); 195 $content.css('min-height',''); // remove min-height again 196 }, true); 197 }; 198 199 // the state indicator 200 $clicky = jQuery(document.createElement('strong')); 201 202 // click function 203 $handle.css('cursor','pointer') 204 .on('click', $handle[0].setState) 205 .prepend($clicky); 206 207 // initial state 208 $handle[0].setState(state); 209 }, 210 211 /** 212 * Provides simple copy-to-clipboard functionality for code blocks. 213 * 214 * This is supposed to work only in secure context, which is required 215 * by the Clipboard API. Intentionally there is no fallback using 216 * the deprecated document.execCommand() 217 */ 218 copyCode: function () { 219 /** 220 * Wrap code block in a container and attach a copy button 221 * @param {Element} code 222 */ 223 function addCopyButton(code) { 224 const copyButton = document.createElement("button"); 225 copyButton.classList.add("code-copy-btn"); 226 copyButton.innerText = LANG["clipboard_button"]; 227 228 copyButton.addEventListener("click", copyToClipboard); 229 code.appendChild(copyButton); 230 } 231 232 /** 233 * Copy the code into system clipboard 234 * 235 * @returns {Promise<void>} 236 */ 237 async function copyToClipboard(){ 238 const preElement = this.closest('pre.code, pre.file'); 239 240 let textToCopy = ""; 241 242 // code with line numbers requires special unpacking to keep line breaks, we read it line by line 243 let codeLines = preElement.querySelectorAll("li > div"); 244 if (codeLines.length === 0) { 245 textToCopy = preElement.innerText; // normal code block 246 } else { 247 textToCopy += Array.from(codeLines) 248 .map(line => line.innerText) 249 .join("\n"); 250 } 251 252 // copy text and show operation status in the button 253 try { 254 await navigator.clipboard.writeText(textToCopy); 255 this.textContent = LANG["clipboard_success"]; 256 } catch (err) { 257 this.textContent = LANG["clipboard_error"]; 258 } finally { 259 setTimeout(() => { 260 this.textContent = LANG["clipboard_button"]; // reset button text after a delay 261 }, 2000); 262 } 263 } 264 265 // do nothing unless we have code blocks and access to the Clipboard object (only in secure context) 266 const codeBlocks = document.querySelectorAll("pre.code, pre.file"); 267 if (codeBlocks.length > 0 && typeof navigator.clipboard === "object") { 268 codeBlocks.forEach(addCopyButton); 269 } 270 } 271}; 272 273jQuery(dw_page.init); 274