1/* DOKUWIKI:include js/skip-link-focus-fix.js */ 2 3jQuery(document).ready(function() { 4 /* 5 * Click to toggle sidebar. 6 */ 7 function toggleSidebar() { 8 jQuery( '#writr__sidebar' ).on( 'click', '#writr__sidebar-toggle', function( e ) { 9 e.preventDefault(); 10 jQuery( 'html, body' ).scrollTop( 0 ); 11 jQuery( this ).toggleClass( 'open' ); 12 jQuery( 'body' ).toggleClass( 'sidebar-closed' ); 13 jQuery( '#writr__secondary' ).resize(); 14 } ); 15 } 16 17 /** 18 * Handles toggling the navigation menu for small screens. 19 */ 20 function toggleNavigation() { 21 var $container = jQuery('#writr__site-navigation'); 22 if (!$container.length) return; 23 var $button = jQuery('.menu-toggle', $container); 24 if (!$button.length) return; 25 var $menu = jQuery('ul', $container); 26 if (!$menu.length) { 27 $menu.hide(); 28 return; 29 } 30 $button.click(function(){ 31 $container.toggleClass('toggled'); 32 }); 33 } 34 35 /* 36 * A function to enable/disable a dropdown submenu. 37 */ 38 function toggleSubmenu() { 39 jQuery( '.main-navigation .node > div > a' ).append( '<span class="dropdown-icon" />' ); 40 jQuery( '#writr__site-navigation' ).on( 'click', '.dropdown-icon', function( e ) { 41 e.preventDefault(); 42 jQuery( this ).toggleClass( 'open' ); 43 if ( jQuery( this ).hasClass( 'open' ) ) { 44 jQuery( this ).parent().parent().next( 'ul' ).show(); 45 } else { 46 jQuery( this ).parent().parent().next( 'ul' ).hide(); 47 } 48 } ); 49 } 50 51 /* 52 * Close TOC by default 53 */ 54 function closeToc() { 55 var $toc = jQuery('#dw__toc .toggle'); 56 if($toc.length) { 57 $toc[0].setState(-1); 58 } 59 } 60 61 /* 62 * Change search submit input to submit button to make it easier to style 63 * @deprecated since Detritus 64 */ 65 function changeSearchInput() { 66 var $searchForm = jQuery('.search-form > form > div'); 67 var $searchButton = jQuery('input[type="submit"]', $searchForm).detach(); 68 var title = $searchButton.attr('title'); 69 var value = $searchButton.val(); 70 $searchForm.append('<button type="submit" title="'+title+'">'+value+'</button>'); 71 } 72 73 /* 74 * Enable add new page dropdown 75 */ 76 function enableAddNewPage() { 77 jQuery('.action.AddNewPage').click(function(event) { 78 event.preventDefault(); 79 const button = jQuery(this); 80 jQuery('.addnewpage').toggle(0,function(){ 81 // set aria-expanded attribute based on visibility 82 button.attr('aria-expanded', jQuery(this).is(':visible')); 83 }); 84 }); 85 86 jQuery(document).click(function(event) { 87 if (!jQuery(event.target).closest('.action.AddNewPage, .addnewpage').length) { 88 jQuery('.addnewpage').hide(); 89 } 90 }); 91 } 92 93 /* 94 * Enable translation dropdown 95 */ 96 function enableTranslation() { 97 jQuery('.action.Translation').click(function(event) { 98 event.preventDefault(); 99 const button = jQuery(this); 100 jQuery('.plugin_translation').toggle(0,function(){ 101 // set aria-expanded attribute based on visibility 102 button.attr('aria-expanded', jQuery(this).is(':visible')); 103 }); 104 }); 105 106 jQuery(document).click(function(event) { 107 if (!jQuery(event.target).closest('.action.Translation, .plugin_translation').length) { 108 jQuery('.plugin_translation').hide(); 109 } 110 }); 111 } 112 113 /* 114 * Enable Toolbar Dropdowns 115 */ 116 function enableToolbarDropdowns() { 117 jQuery('#writr__toolbar .hook .node').each(function() { 118 const dropdown = jQuery(this); 119 dropdown.find('div.li').click(function(event) { 120 const trigger = jQuery(this); 121 dropdown.find('> ul').toggle(0,function(){ 122 trigger.attr('aria-expanded', jQuery(this).is(':visible')); 123 }); 124 125 // Close dropdown when clicking outside 126 jQuery(document).on('click.dropdown', function(e) { 127 if (!dropdown.is(e.target) && dropdown.has(e.target).length === 0) { 128 dropdown.find('> ul').hide(); 129 trigger.attr('aria-expanded', 'false'); 130 jQuery(document).off('click.dropdown'); 131 } 132 }); 133 }); 134 }); 135 } 136 137 /* 138 * Enable Collapse 139 */ 140 function enableCollapse() { 141 jQuery('[data-toggle="collapse"]').click(function(event){ 142 event.preventDefault(); 143 const trigger = jQuery(this); 144 const target = jQuery(trigger.attr('data-target')); 145 target.slideToggle('fast',function(){ 146 // set aria-expanded attribute based on visibility 147 trigger.attr('aria-expanded', target.is(':visible')); 148 }); 149 }); 150 } 151 152 /* 153 * Enable Dropdowns 154 */ 155 function enableDropdowns() { 156 jQuery('.dropdown').each(function() { 157 const dropdown = jQuery(this); 158 dropdown.find('[data-toggle="dropdown"]').click(function(event) { 159 event.preventDefault(); 160 const button = jQuery(this); 161 dropdown.find('.dropdown-menu').toggle(0,function(){ 162 button.attr('aria-expanded', jQuery(this).is(':visible')); 163 }); 164 165 // Close dropdown when clicking outside 166 jQuery(document).on('click.dropdown', function(e) { 167 if (!dropdown.is(e.target) && dropdown.has(e.target).length === 0) { 168 dropdown.find('> ul').hide(); 169 button.attr('aria-expanded', 'false'); 170 jQuery(document).off('click.dropdown'); 171 } 172 }); 173 }); 174 }); 175 } 176 177 /* 178 * Enable Tooltips 179 */ 180 function enableTooltips() { 181 jQuery('body.enableTooltips [title]:not(.media):not(img):not([title=""]), body.enableTooltips [alt]:not(.media):not(img):not([alt=""])').each(function() { 182 const element = jQuery(this); 183 const content = element.attr('alt') ? element.attr('alt') : element.attr('title'); 184 element.attr('data-tooltip-content', content); 185 186 element.hover(function() { 187 // Prevent default browser tooltip from showing 188 const tooltipType = element.attr('alt') ? 'alt' : 'title'; 189 element.removeAttr(tooltipType).attr('data-tooltip-type', tooltipType); 190 191 // Create and append the tooltip 192 const tooltip = jQuery('<div class="tooltip"><div class="tooltip-text">' + content + '</div></div>'); 193 jQuery('body').append(tooltip); 194 195 // Calculate and set the position of the tooltip 196 const elementOffset = element.offset(); 197 const tooltipWidth = tooltip.outerWidth(); 198 const elementWidth = element.outerWidth(); 199 const topPosition = elementOffset.top + element.outerHeight() + 10; // Adjust +10 for spacing 200 const leftPosition = elementOffset.left + (elementWidth / 2) - (tooltipWidth / 2); 201 202 tooltip.css({ 203 top: topPosition, 204 left: leftPosition, 205 display: 'inline-block' 206 }); 207 208 }, function() { 209 // Restore the original attribute and remove the tooltip 210 element.attr(element.attr('data-tooltip-type'), content); 211 jQuery('.tooltip').remove(); 212 }); 213 }); 214 } 215 216 /* 217 * Enable Improved File Input 218 */ 219 function enableFileInput() { 220 jQuery('input[type="file"]').on('change', function() { 221 var input = jQuery(this); 222 var label = input.prev('span'); 223 var group = input.parent('label'); 224 var fileName = input.val().split('\\').pop() || 'No file chosen'; 225 226 // Check if a div already exists 227 if (input.next('div').length > 0) { 228 input.next('div').remove(); 229 } 230 231 // Create a div after the input 232 var file = jQuery('<div style="text-align:center;width:100%"></div>'); 233 234 // Update the text of the file-name span 235 file.prepend(fileName); 236 237 // Add a button to clear the field 238 var button = jQuery('<button type="button" class="tagerror" style="margin-left:4px;padding: 4px 8px;font-size: 80%;"><i class="bi bi-trash"></i></button>').appendTo(file); 239 240 // Add Click event on the button 241 button.click(function(){ 242 243 // Clear value 244 input.val(''); 245 246 // Remove the file object 247 file.remove(); 248 249 // Show input and label 250 label.show(); 251 input.show(); 252 }); 253 254 // Insert the div after the input 255 input.after(file); 256 257 // Hide label & input 258 label.hide(); 259 input.hide(); 260 }); 261 } 262 263 /* 264 * Disable Newlines in Textareas 265 */ 266 function disableNewlines() { 267 // Check if "?do=" is present in the URL 268 if (window.location.search.indexOf("?do=") === -1) { 269 jQuery("form").on("submit", function(event) { 270 // Prevent the form from submitting immediately 271 event.preventDefault(); 272 273 // Find the textarea and replace newlines with " \\ " 274 var textarea = jQuery(this).find('textarea'); 275 textarea.val(textarea.val().replace(/\n/gm, ' \\\\\\ ')); 276 277 // After running the function, manually trigger the form submission 278 this.submit(); 279 }); 280 } 281 } 282 283 /* 284 * Run the functions 285 */ 286 jQuery(function(){ 287 toggleSidebar(); 288 toggleNavigation(); 289 toggleSubmenu(); 290 closeToc(); 291 changeSearchInput(); 292 enableAddNewPage(); 293 enableTranslation(); 294 enableToolbarDropdowns(); 295 enableDropdowns(); 296 enableTooltips(); 297 enableCollapse(); 298 enableFileInput(); 299 }); 300 301 /* 302 * NSPAGES enable multi-line support 303 */ 304 jQuery('.nspagesPicturesModeTitle').each(function() { 305 if (jQuery(this).prop('scrollHeight') > 32) { 306 jQuery(this).addClass('multi-line'); 307 } 308 }); 309 310 /* 311 * Disqus iframe minimum height 312 * 313 * Disqus sets an inline height with !important (via JS). That height can be a bit 314 * too small for our visual caps/padding. We therefore enforce: 315 * effectiveHeight >= (disqusHeight + EXTRA) 316 * 317 * Important: we must NOT keep adding EXTRA on our own updates. 318 */ 319 (function disqusMinHeightFix(){ 320 const $thread = jQuery('#disqus__thread'); 321 if (!$thread.length) return; 322 323 const EXTRA = 72; // additional space needed for our visual caps/spacing 324 325 function parseInlinePx(value) { 326 if (!value) return null; 327 const n = parseInt(String(value).replace('px', ''), 10); 328 return Number.isFinite(n) ? n : null; 329 } 330 331 function getInlineHeightPx(iframe) { 332 // Prefer inline style.height because Disqus writes it. 333 const h = parseInlinePx(iframe.style && iframe.style.height); 334 if (h !== null) return h; 335 336 // Fallback: jQuery height 337 const $if = jQuery(iframe); 338 const jh = parseInlinePx($if.css('height')); 339 return jh !== null ? jh : null; 340 } 341 342 function applyFloor(iframe) { 343 if (!iframe) return; 344 345 const current = getInlineHeightPx(iframe); 346 if (current === null) return; 347 348 // If this height is exactly what we last applied, ignore (prevents +64 runaway). 349 const lastApplied = parseInlinePx(iframe.getAttribute('data-writr-last-applied')); 350 if (lastApplied !== null && current === lastApplied) return; 351 352 // Treat the current inline height as Disqus' desired height. 353 const disqusHeight = current; 354 const target = disqusHeight + EXTRA; 355 356 // Only increase if needed. 357 if (current < target) { 358 iframe.style.setProperty('height', target + 'px', 'important'); 359 iframe.setAttribute('data-writr-last-applied', String(target)); 360 } 361 362 // Keep a record of the last disqus height we saw. 363 iframe.setAttribute('data-writr-last-disqus', String(disqusHeight)); 364 } 365 366 function attachToIframe(iframe) { 367 if (!iframe) return; 368 369 // Avoid attaching twice. 370 if (iframe.getAttribute('data-writr-disqus-observer') === '1') { 371 applyFloor(iframe); 372 return; 373 } 374 iframe.setAttribute('data-writr-disqus-observer', '1'); 375 376 // Initial apply. 377 applyFloor(iframe); 378 379 // Observe style changes (Disqus updates height via inline styles). 380 const obs = new MutationObserver(function(mutations){ 381 for (const m of mutations) { 382 if (m.type === 'attributes' && m.attributeName === 'style') { 383 applyFloor(iframe); 384 } 385 } 386 }); 387 obs.observe(iframe, { attributes: true, attributeFilter: ['style'] }); 388 389 // Store observer so we can disconnect if needed. 390 iframe._writrDisqusObs = obs; 391 } 392 393 function findPrimaryIframe() { 394 // Disqus uses an iframe id like dsq-app####. 395 const iframe = $thread.find('iframe[id^="dsq-app"]').get(0); 396 return iframe || null; 397 } 398 399 // Attach to current iframe. 400 attachToIframe(findPrimaryIframe()); 401 402 // In some cases Disqus replaces the iframe node. Watch the container for changes. 403 const containerObs = new MutationObserver(function(){ 404 const iframe = findPrimaryIframe(); 405 if (iframe) attachToIframe(iframe); 406 }); 407 containerObs.observe($thread.get(0), { childList: true, subtree: true }); 408 409 // As a last resort, re-apply on window resize (layout can change Disqus height). 410 jQuery(window).on('resize', function(){ 411 const iframe = findPrimaryIframe(); 412 if (iframe) applyFloor(iframe); 413 }); 414 })(); 415}); 416