1// /js/list-status.js 2// Header heart icon + offcanvas slide-out for quick list access. 3// Hidden until the user has at least one list. 4// Loaded as a regular deferred script (not a module) so it registers 5// with Alpine before Alpine processes the DOM. 6 7document.addEventListener('alpine:init', () => { 8 // Shared store so both mobile and desktop icons can read visibility/count 9 Alpine.store('listStatus', { 10 visible: false, 11 totalItemCount: 0, 12 defaultListId: 0 13 }); 14 15 Alpine.data('listStatus', () => ({ 16 lists: [], 17 activeList: null, 18 items: [], 19 loaded: false, 20 slideOutOpen: false, 21 loadingItems: false, 22 shareCopyMessage: '', 23 copyingShareLink: false, 24 25 get visible() { 26 return Alpine.store('listStatus').visible; 27 }, 28 29 get totalItemCount() { 30 return Alpine.store('listStatus').totalItemCount; 31 }, 32 33 get defaultListId() { 34 return Alpine.store('listStatus').defaultListId; 35 }, 36 37 async init() { 38 await this._loadLists(); 39 40 // Favorites keeps the shared cache in sync before firing this. 41 document.addEventListener('list-status-update', () => this._loadLists()); 42 43 const el = document.getElementById('listStatusSlideOut'); 44 if (el) { 45 el.addEventListener('show.bs.offcanvas', () => { 46 this._loadLists({ force: true }).then(() => this._loadItems()); 47 }); 48 el.addEventListener('shown.bs.offcanvas', () => { this.slideOutOpen = true; }); 49 el.addEventListener('hidden.bs.offcanvas', () => { this.slideOutOpen = false; }); 50 } 51 }, 52 53 async _loadLists({ force = false } = {}) { 54 try { 55 const { getListsOnce } = await import(Utils.baseUrl + 'js/quicklist/lists-cache.js'); 56 const data = await getListsOnce({ force }); 57 const defaultListId = Number(data?.defaultListId || 0); 58 this.lists = (data?.lists || []).map(l => ({ 59 id: Number(l.id), 60 title: l.title ?? '', 61 isPrimary: !!l.isPrimary, 62 itemCount: l.itemCount, 63 isPublic: !!l.isPublic, 64 publicLinkUrl: l.publicLinkUrl ?? '' 65 })); 66 this.loaded = true; 67 68 Alpine.store('listStatus').defaultListId = defaultListId; 69 70 const defaultList = defaultListId > 0 71 ? this.lists.find(l => l.id === defaultListId) 72 : this.lists.find(l => l.isPrimary); 73 74 if (defaultList) { 75 this.activeList = defaultList; 76 } else if (this.lists.length && !this.activeList) { 77 this.activeList = this.lists[0]; 78 } else if (this.activeList) { 79 const updated = this.lists.find(l => l.id === this.activeList.id); 80 this.activeList = updated || this.lists[0] || null; 81 } 82 83 Alpine.store('listStatus').visible = this.lists.length > 0; 84 Alpine.store('listStatus').totalItemCount = this.activeList?.itemCount ?? 0; 85 } catch (e) { 86 // Silently fail â icon stays hidden 87 } 88 }, 89 90 async _loadItems() { 91 if (!this.activeList) { this.items = []; return; } 92 this.loadingItems = true; 93 try { 94 const res = await fetch(Utils.baseUrl + 'quickList/getlistitems?listId=' + this.activeList.id + '&max=10', { credentials: 'include' }); 95 if (!res.ok) { this.items = []; return; } 96 const data = await res.json(); 97 this.items = data?.items || []; 98 } catch (e) { 99 this.items = []; 100 } finally { 101 this.loadingItems = false; 102 } 103 }, 104 105 async selectList(list) { 106 this.activeList = list; 107 Alpine.store('listStatus').totalItemCount = list?.itemCount ?? 0; 108 await this._loadItems();
109 }, 110 111 async copyShareLink() { 112 if (!this.activeList?.publicLinkUrl || this.copyingShareLink) return; 113 114 this.copyingShareLink = true; 115 116 try { 117 await navigator.clipboard.writeText(this.activeList.publicLinkUrl); 118 this.shareCopyMessage = 'Link copied.'; 119 window.setTimeout(() => { 120 this.shareCopyMessage = ''; 121 }, 2000); 122 } catch (e) { 123 this.shareCopyMessage = 'Unable to copy the link.'; 124 } finally { 125 this.copyingShareLink = false; 126 } 127 }, 128 129 toggleSlideOut() { 130 const el = document.getElementById('listStatusSlideOut'); 131 if (!el) return; 132 const instance = bootstrap.Offcanvas.getOrCreateInstance(el); 133 if (this.slideOutOpen) { 134 instance.hide(); 135 } else { 136 this._loadLists({ force: true }).then(() => this._loadItems()); 137 instance.show(); 138 } 139 }, 140 141 viewAllListsUrl() { 142 return Utils.baseUrl + 'quicklist/list'; 143 }, 144 145 viewListUrl(list) { 146 return Utils.baseUrl + 'quicklist/index/' + (list?.id || this.activeList?.id); 147 } 148 })); 149}); 150 151// Global function for triggers outside the Alpine component scope 152window.toggleListStatus = function() { 153 const el = document.getElementById('listStatusSlideOut'); 154 if (!el) return; 155 const instance = bootstrap.Offcanvas.getOrCreateInstance(el); 156 instance.toggle(); 157};
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.