1/** 2 * PP Collapsible Menu Component 3 * ============================= 4 * Merkezi açılır/kapanır menü yönetimi 5 * Tüm dropdown menüler (hamburger, user menu vb.) bu modülü kullanır 6 * 7 * Kullanım: 8 * HTML: 9 * <div data-pp-collapsible="menu-id" class="pp-collapsible-wrap"> 10 * <button data-pp-collapsible-toggle="menu-id" class="pp-collapsible-toggle"> 11 * <i class="fa fa-folder"></i> 12 * <span>Menü BaÅlıÄı</span> 13 * <i class="fa fa-chevron-down pp-collapsible-chevron"></i> 14 * </button> 15 * <div data-pp-collapsible-content="menu-id" class="pp-collapsible-content" hidden> 16 * <!-- İçerik --> 17 * </div> 18 * </div> 19 * 20 * JavaScript: 21 * ppCollapsible.toggle('menu-id'); 22 * ppCollapsible.open('menu-id'); 23 * ppCollapsible.close('menu-id'); 24 * ppCollapsible.setItems('menu-id', [...items]); 25 */ 26(function() { 27 'use strict'; 28 29 // Selectors 30 const SELECTORS = { 31 parent: '[data-pp-collapsible]', 32 toggle: '[data-pp-collapsible-toggle]', 33 content: '[data-pp-collapsible-content]', 34 chevron: '.pp-collapsible-chevron' 35 }; 36 37 // State tracking 38 const boundElements = new WeakSet(); 39 40 /** 41 * Get content element by ID 42 */ 43 function getContent(id) {
44 return document.querySelector(`[data-pp-collapsible-content="${id}"]`); 45 } 46 47 /** 48 * Get toggle button by ID 49 */ 50 function getToggle(id) { 51 return document.querySelector(`[data-pp-collapsible-toggle="${id}"]`); 52 } 53 54 /** 55 * Check if menu is open 56 */ 57 function isOpen(id) { 58 const content = getContent(id); 59 return content ? content.classList.contains('open') : false; 60 } 61 62 /** 63 * Open a specific menu 64 */ 65 function open(id) { 66 const content = getContent(id); 67 const btn = getToggle(id); 68 69 if (!content) { 70 return false; 71 } 72 73 74 // Remove hidden attribute completely (not just set to false) 75 content.removeAttribute('hidden'); 76 content.hidden = false; 77 content.classList.add('open'); 78 79 // Force display 80 content.style.display = 'grid'; 81 82 if (btn) { 83 btn.setAttribute('aria-expanded', 'true'); 84 } 85 86 // Dispatch custom event 87 content.dispatchEvent(new CustomEvent('pp-collapsible:open', { 88 bubbles: true, 89 detail: { id } 90 })); 91 92 return true; 93 } 94 95 /** 96 * Close a specific menu 97 */ 98 function close(id) { 99 const content = getContent(id); 100 const btn = getToggle(id); 101 102 if (!content) return false; 103 104 content.classList.remove('open'); 105 content.hidden = true; 106 content.style.display = 'none'; 107 108 if (btn) { 109 btn.setAttribute('aria-expanded', 'false'); 110 } 111 112 // Close all nested collapsibles 113 const nestedContents = content.querySelectorAll(SELECTORS.content); 114 nestedContents.forEach(nested => { 115 const nestedId = nested.getAttribute('data-pp-collapsible-content'); 116 if (nestedId) { 117 close(nestedId); 118 } 119 }); 120 121 // Dispatch custom event 122 content.dispatchEvent(new CustomEvent('pp-collapsible:close', { 123 bubbles: true, 124 detail: { id } 125 })); 126 127 return true; 128 } 129 130 /** 131 * Toggle a specific menu 132 */ 133 function toggle(id) { 134 if (isOpen(id)) { 135 return close(id); 136 } else { 137 return open(id); 138 } 139 } 140 141 /** 142 * Close all collapsible menus 143 */ 144 function closeAll() { 145 const allContents = document.querySelectorAll(SELECTORS.content); 146 allContents.forEach(content => { 147 const id = content.getAttribute('data-pp-collapsible-content'); 148 if (id) { 149 close(id); 150 } 151 }); 152 } 153 154 /** 155 * Set items dynamically for a collapsible menu 156 * @param {string} id - Menu ID 157 * @param {Array} items - Array of item objects 158 * @param {string} items[].type - 'link', 'button', 'divider', or 'collapsible' 159 * @param {string} items[].icon - Font Awesome icon class 160 * @param {string} items[].label - Display text 161 * @param {string} items[].href - URL for links 162 * @param {Function} items[].onClick - Click handler for buttons 163 * @param {string} items[].badge - Badge text (optional) 164 * @param {string} items[].badgeId - Badge element ID for dynamic updates (optional) 165 * @param {Array} items[].children - Nested items for collapsible type 166 */ 167 function setItems(id, items) { 168 const content = getContent(id); 169 if (!content || !Array.isArray(items)) return false; 170 171 content.innerHTML = ''; 172 173 items.forEach((item, index) => { 174 const element = createItemElement(item, `${id}-item-${index}`); 175 if (element) { 176 content.appendChild(element); 177 } 178 }); 179 180 // Reinitialize event bindings for new elements 181 init(); 182 183 return true; 184 } 185 186 /** 187 * Create DOM element for an item 188 */ 189 function createItemElement(item, uniqueId) { 190 if (!item || !item.type) return null; 191 192 switch (item.type) { 193 case 'divider': 194 const divider = document.createElement('div'); 195 divider.className = 'pp-collapsible-divider'; 196 return divider; 197 198 case 'link': 199 const link = document.createElement('a'); 200 link.href = item.href || '#'; 201 link.className = item.nested ? 'pp-collapsible-nested-item' : 'pp-collapsible-item'; 202 link.title = item.label || ''; 203 link.style.textDecoration = 'none'; 204 205 if (item.icon) { 206 const icon = document.createElement('i'); 207 icon.className = item.icon; 208 link.appendChild(icon); 209 } 210 211 const linkSpan = document.createElement('span'); 212 linkSpan.textContent = item.label || ''; 213 link.appendChild(linkSpan); 214 215 if (item.badge) { 216 const badge = document.createElement('span'); 217 badge.className = 'pp-collapsible-badge';
218 badge.textContent = item.badge; 219 if (item.badgeId) badge.id = item.badgeId; 220 badge.style.display = item.badge === '0' ? 'none' : ''; 221 link.appendChild(badge); 222 } 223 224 if (item.id) link.id = item.id; 225 226 return link; 227 228 case 'button': 229 const button = document.createElement('button'); 230 button.type = 'button'; 231 button.className = item.nested ? 'pp-collapsible-nested-item' : 'pp-collapsible-item'; 232 button.title = item.label || ''; 233 234 if (item.icon) { 235 const btnIcon = document.createElement('i'); 236 btnIcon.className = item.icon; 237 button.appendChild(btnIcon); 238 } 239 240 const btnSpan = document.createElement('span'); 241 btnSpan.textContent = item.label || ''; 242 button.appendChild(btnSpan); 243 244 if (item.badge) { 245 const btnBadge = document.createElement('span'); 246 btnBadge.className = 'pp-collapsible-badge'; 247 btnBadge.textContent = item.badge; 248 if (item.badgeId) btnBadge.id = item.badgeId; 249 btnBadge.style.display = item.badge === '0' ? 'none' : ''; 250 button.appendChild(btnBadge); 251 } 252 253 if (item.onClick) { 254 button.addEventListener('click', item.onClick); 255 } 256 257 if (item.id) button.id = item.id; 258 259 return button; 260 261 case 'collapsible': 262 const wrap = document.createElement('div'); 263 wrap.className = 'pp-collapsible-wrap'; 264 wrap.setAttribute('data-pp-collapsible', uniqueId); 265 266 const toggleBtn = document.createElement('button'); 267 toggleBtn.type = 'button'; 268 toggleBtn.className = 'pp-collapsible-toggle pp-collapsible-item'; 269 toggleBtn.setAttribute('data-pp-collapsible-toggle', uniqueId); 270 toggleBtn.setAttribute('aria-expanded', 'false'); 271 toggleBtn.setAttribute('aria-controls', `${uniqueId}-content`); 272 toggleBtn.title = item.label || ''; 273 274 if (item.icon) { 275 const toggleIcon = document.createElement('i'); 276 toggleIcon.className = item.icon; 277 toggleBtn.appendChild(toggleIcon); 278 } 279 280 const toggleSpan = document.createElement('span'); 281 toggleSpan.textContent = item.label || ''; 282 toggleBtn.appendChild(toggleSpan); 283 284 if (item.badge) { 285 const toggleBadge = document.createElement('span'); 286 toggleBadge.className = 'pp-collapsible-badge'; 287 toggleBadge.textContent = item.badge; 288 if (item.badgeId) toggleBadge.id = item.badgeId; 289 toggleBadge.style.display = item.badge === '0' ? 'none' : ''; 290 toggleBtn.appendChild(toggleBadge); 291 } 292 293 const chevron = document.createElement('i'); 294 chevron.className = 'fa fa-chevron-down pp-collapsible-chevron'; 295 chevron.setAttribute('aria-hidden', 'true'); 296 toggleBtn.appendChild(chevron); 297 298 wrap.appendChild(toggleBtn); 299 300 const nestedContent = document.createElement('div'); 301 nestedContent.id = `${uniqueId}-content`; 302 nestedContent.className = 'pp-collapsible-content'; 303 nestedContent.setAttribute('data-pp-collapsible-content', uniqueId); 304 nestedContent.setAttribute('role', 'region'); 305 nestedContent.setAttribute('aria-label', `${item.label || ''} alt menü`); 306 nestedContent.hidden = true; 307 308 if (item.children && Array.isArray(item.children)) { 309 item.children.forEach((child, childIndex) => { 310 child.nested = true; 311 const childElement = createItemElement(child, `${uniqueId}-child-${childIndex}`); 312 if (childElement) { 313 nestedContent.appendChild(childElement); 314 } 315 }); 316 } 317 318 wrap.appendChild(nestedContent); 319 320 if (item.id) wrap.id = item.id; 321 322 return wrap; 323 324 default: 325 return null; 326 } 327 } 328 329 /** 330 * Bind event listeners to a toggle button 331 */ 332 function bindToggle(toggleBtn) { 333 if (!toggleBtn || boundElements.has(toggleBtn)) return; 334 335 const id = toggleBtn.getAttribute('data-pp-collapsible-toggle'); 336 if (!id) return; 337 338 339 // Click handler 340 toggleBtn.addEventListener('click', function(e) { 341 e.preventDefault(); 342 e.stopPropagation(); 343 e.stopImmediatePropagation(); 344 const result = toggle(id); 345 return false;
346 }, true); // Use capture phase to ensure we handle first 347 348 // Touch handler for mobile 349 toggleBtn.addEventListener('touchend', function(e) { 350 e.preventDefault(); 351 e.stopPropagation(); 352 e.stopImmediatePropagation(); 353 toggle(id); 354 return false; 355 }, { capture: true, passive: false }); 356 357 boundElements.add(toggleBtn); 358 } 359 360 /** 361 * Initialize all collapsible menus 362 */ 363 function init() { 364 const toggles = document.querySelectorAll(SELECTORS.toggle); 365 toggles.forEach((t, i) => { 366 }); 367 toggles.forEach(bindToggle); 368 369 // Ensure all content elements start with correct closed state 370 const contents = document.querySelectorAll(SELECTORS.content); 371 contents.forEach(content => { 372 if (!content.classList.contains('open')) { 373 content.style.display = 'none'; 374 content.removeAttribute('hidden'); 375 } 376 }); 377 } 378 379 /** 380 * Run initialization when DOM is ready 381 */ 382 function ready() { 383 if (document.readyState === 'loading') { 384 document.addEventListener('DOMContentLoaded', init, { once: true }); 385 } else { 386 init(); 387 } 388 389 // Also reinitialize after full page load to catch any late-rendered elements 390 window.addEventListener('load', function() { 391 setTimeout(init, 50); 392 }, { once: true }); 393 } 394 395 // Auto-initialize 396 ready(); 397 398 // Global capture-phase click handler as fallback 399 // This ensures collapsible toggles are handled even if individual binding fails 400 if (!window.__ppCollapsibleGlobalHandler) { 401 document.addEventListener('click', function(e) { 402 const toggle = e.target.closest('[data-pp-collapsible-toggle]'); 403 if (toggle) { 404 const id = toggle.getAttribute('data-pp-collapsible-toggle'); 405 if (id) { 406 e.preventDefault(); 407 e.stopPropagation(); 408 e.stopImmediatePropagation(); 409 window.ppCollapsible.toggle(id); 410 } 411 } 412 }, true); // Capture phase 413 window.__ppCollapsibleGlobalHandler = true; 414 } 415 416 // Expose public API 417 window.ppCollapsible = { 418 init, 419 toggle, 420 open, 421 close, 422 closeAll, 423 isOpen, 424 setItems, 425 getContent, 426 getToggle 427 }; 428 429})();
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.