1/** 2 * @file 3 * CKEditor Accordion functionality. 4 */ 5 6(function (Drupal, drupalSettings) { 7 'use strict'; 8 9 let animating = false; 10 let animateObjects = []; 11 12 // Default config. 13 const defaults = { 14 duration: 300, 15 easing: (t, b, c, d) => { 16 if ((t /= d / 2) < 1) 17 return c / 2 * t * t + b; 18 return -c / 2 * ((--t) * (t - 2) - 1) + b; 19 } 20 }; 21 22 // Two directions. 23 const directions = { 24 OPEN: 1, 25 CLOSE: 2 26 }; 27 28 // Animate fn. 29 const animate = (animateSets, now) => { 30 let animationContinue = false; 31 for(let x = 0; x < animateSets.length; x++) { 32 let element = animateSets[x][0]; 33 let options = animateSets[x][1]; 34 if (!options.startTime) { 35 options.startTime = now; 36 } 37 let currentTime = now - options.startTime; 38 animationContinue = currentTime < options.duration; 39 if (animationContinue) { 40 let newHeight = options.easing(currentTime, options.startingHeight, options.distanceHeight, options.duration); 41 element.style.height = `${newHeight.toFixed(2)}px`; 42 } 43 } 44 45 if (animationContinue) { 46 window.requestAnimationFrame((timestamp) => animate(animateSets, timestamp)); 47 } 48 else { 49 for(let x = 0; x < animateSets.length; x++) { 50 let element = animateSets[x][0]; 51 let options = animateSets[x][1]; 52 if (options.direction === directions.CLOSE) { 53 element.style.display = 'none'; 54 } 55 if (options.direction === directions.OPEN) { 56 element.style.display = 'block'; 57 } 58 59 // Remove element animation styles. 60 element.style.height = null; 61 element.style.overflow = null; 62 element.style.marginTop = null; 63 element.style.marginBottom = null; 64 element.style.paddingTop = null; 65 element.style.paddingBottom = null; 66 } 67 68 // Reset vars. 69 animateObjects = []; 70 animating = false; 71 } 72 } 73 74 // Animation handler. 75 const doAnimations = (animateObjects) => { 76 let doAnimate = drupalSettings.ckeditorAccordion.accordionStyle.animateAccordionOpenAndClose ?? true; 77 if (!doAnimate) { 78 animateObjects.forEach(object => { 79 const element = object[1]; 80 if (object[0] == 'slideUp') { 81 element.style.display = 'none'; 82 } 83 else { 84 element.style.display = 'block'; 85 } 86 }); 87 animating = false; 88 } 89 else { 90 let animateSets = []; 91 animateObjects.forEach(object => { 92 const element = object[1]; 93 let options = Object.assign({}, defaults); 94 if (object[0] == 'slideUp') { 95 options.direction = directions.CLOSE; 96 options.to = 0; 97 options.startingHeight = element.scrollHeight; 98 options.distanceHeight = -options.startingHeight; 99 100 // Set element animation styles. 101 element.style.display = 'block'; 102 element.style.overflow = 'hidden'; 103 element.style.marginTop = '0'; 104 element.style.marginBottom = '0'; 105 element.style.paddingTop = '0'; 106 element.style.paddingBottom = '0'; 107 } else { 108 element.style.height = '0px'; 109 110 // Set element animation styles. 111 element.style.display = 'block'; 112 element.style.overflow = 'hidden'; 113 element.style.marginTop = '0'; 114 element.style.marginBottom = '0'; 115 element.style.paddingTop = '0'; 116 element.style.paddingBottom = '0'; 117 118 options.direction = directions.OPEN; 119 options.to = element.scrollHeight; 120 options.startingHeight = 0; 121 options.distanceHeight = options.to; 122 } 123 124 delete options.startTime; 125 animateSets.push([element, options]); 126 }); 127 128 window.requestAnimationFrame((timestamp) => animate(animateSets, timestamp)); 129 } 130 } 131 132 Drupal.behaviors.ckeditorAccordion = { 133 134 attach: function (context, settings) { 135 const $ckeditorAccordions = once('ckeditorAccordions', '.ckeditor-accordion', context); 136 const doAnimate = drupalSettings.ckeditorAccordion.accordionStyle.animateAccordionOpenAndClose ?? true;
137 const openTabsWithHash = drupalSettings.ckeditorAccordion.accordionStyle.openTabsWithHash ?? false; 138 for (let i = 0; i < $ckeditorAccordions.length; i++) { 139 let $accordion = $ckeditorAccordions[i]; 140 141 // The first one is the correct one. 142 if (!drupalSettings.ckeditorAccordion.accordionStyle.collapseAll) { 143 $accordion.querySelector('dt:first-child').classList.add('active'); 144 let dd = $accordion.querySelector('dd:first-of-type'); 145 dd.classList.add('active'); 146 dd.style.display = 'block'; 147 } 148 149 // Turn the accordion tabs to links so that the content is accessible & can be traversed using keyboard. 150 let childDts = Array.from($accordion.children).filter(function (child) { 151 return (child.tagName.toLowerCase() == 'dt'); 152 }); 153 for (let x = 0; x < childDts.length; x++) { 154 let $tab = childDts[x]; 155 let tabText = $tab.innerText.trim(); 156 let toggleClass = $tab.classList.contains('active') ? ' active' : ''; 157 let hrefAndIds = 'href="#"'; 158 if (openTabsWithHash) { 159 let tabHash = encodeURIComponent(tabText.replace(/[^A-Za-z0-9]/g, "")); 160 hrefAndIds = 'href="#' + tabHash + '" id="' + tabHash + '" onclick="return false;"'; 161 } 162 $tab.innerHTML = '<a class="ckeditor-accordion-toggler" ' + hrefAndIds + '><span class="ckeditor-accordion-toggle' + toggleClass + '"></span>' + tabText + '</a>'; 163 } 164 165 // Wrap the accordion in a div element so that quick edit function shows the source correctly. 166 $accordion.classList.add('styled'); 167 $accordion.classList.remove('ckeditor-accordion'); 168 let $wrapper = document.createElement('div'); 169 $wrapper.classList.add('ckeditor-accordion-container'); 170 if (!doAnimate) { 171 $wrapper.classList.add('no-animations'); 172 } 173 $accordion.after($wrapper); 174 $wrapper.appendChild($accordion); 175 176 // Trigger an ckeditorAccordionAttached event to let other frameworks know that the accordion has been attached. 177 let eventAccordionAttached = new Event('ckeditorAccordionAttached'); 178 $accordion.dispatchEvent(eventAccordionAttached); 179 180 // Handle click events. 181 let $togglers = $accordion.querySelectorAll('.ckeditor-accordion-toggler'); 182 for (let x = 0; x < $togglers.length; x++) { 183 $togglers[x].addEventListener("click", function(e) { 184 let $title = this.parentNode; 185 let $content = $title.nextElementSibling; 186 let $parent = $title.parentNode; 187 188 if (animating) { 189 // Prevent defaults. 190 e.preventDefault(); 191 return; 192 } 193 animating = true; 194 195 // Clicking on open element, close it. 196 if ($title.classList.contains('active')) { 197 $title.classList.remove('active'); 198 $content.classList.remove('active'); 199 200 // Add to animateObjects. 201 animateObjects.push(['slideUp', $content]); 202 } 203 else { 204 if(!drupalSettings.ckeditorAccordion.accordionStyle.keepRowsOpen) { 205 let activeChilds = Array.from($parent.children).filter(function (child) { 206 return (child.classList.contains('active')); 207 }); 208 activeChilds.forEach(function(activeChild) { 209 // Remove active class from all. 210 activeChild.classList.remove('active'); 211 if (activeChild.tagName.toLowerCase() == 'dd') { 212 // Slide DDs up. 213 animateObjects.push(['slideUp', activeChild]); 214 } 215 }); 216 } 217 218 $title.classList.add('active'); 219 $content.classList.add('active'); 220 221 // Add to animateObjects. 222 animateObjects.push(['slideDown', $content]); 223 } 224 225 doAnimations(animateObjects); 226 227 // Prevent defaults. 228 e.preventDefault(); 229 }); 230 } 231
232 // Open tabs with hash if config requires. 233 if (openTabsWithHash) { 234 // Trigger hash change when clicking an anchor to an accordion tab on the same page. 235 const $hashLinks = document.querySelectorAll('a[href*="#"]:not(.ckeditor-accordion-toggler):not(.visually-hidden)'); 236 for (let x = 0; x < $hashLinks.length; x++) { 237 $hashLinks[x].addEventListener('click', function (e) { 238 var parser = document.createElement('a'), hash; 239 var preventDefault = false; 240 parser.href = this.getAttribute('href'); 241 hash = parser.hash; 242 if (hash) { 243 // Get the matching accordion toggler with hash. 244 var el = document.querySelector('a.ckeditor-accordion-toggler[href="' + hash + '"]'); 245 if (el != null) { 246 // Set / update hash so that the event listener below fires. 247 if (window.location.hash === hash) { 248 window.dispatchEvent(new Event('hashchange')); 249 } 250 else { 251 window.location.hash = hash; 252 } 253 254 preventDefault = true; 255 } 256 } 257 258 if (preventDefault) { 259 e.preventDefault(); 260 } 261 }); 262 } 263 264 // Open content that matches the hash on hash change. 265 window.addEventListener('hashchange', function() { 266 var el = document.querySelector('a.ckeditor-accordion-toggler[href="' + window.location.hash + '"]'); 267 if (el != null && !el.parentNode.classList.contains('active')) { 268 el.click(); 269 } 270 }); 271 // Trigger event once on page load so that the right accordion is open. 272 window.dispatchEvent(new Event('hashchange')); 273 } 274 275 } 276 } 277 }; 278})(Drupal, drupalSettings);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.