PageSourceSearch

https://proparcel.com/static/js/core/components/collapsible_menu.js

js proparcel.com collected 2026-09-24 14:50:43 UTC 12,424 bytes, 429 lines download raw bytes

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.