1/** 2 * Discord-style column resizing for chatroom (channels + members panels). 3 * Desktop only; widths persist in localStorage. 4 */ 5window.ChatroomResize = (function () { 6 'use strict'; 7 8 const STORAGE_KEY = 'pupwife_chatroom_column_widths'; 9 const MOBILE_MAX = 768; 10 const NAV_WIDTH = 88; 11 const CHAT_MIN = 320; 12 const CHANNELS_MIN = 180; 13 const CHANNELS_MAX = 420; 14 const MEMBERS_MIN = 180; 15 const MEMBERS_MAX = 400; 16 const DEFAULT_CHANNELS = 240; 17 const DEFAULT_MEMBERS = 240; 18 19 let booted = false; 20 let channelsHandle = null; 21 let membersHandle = null; 22 let activeDrag = null; 23 let resizeListener = null; 24 25 function isMobile() { 26 return window.innerWidth <= MOBILE_MAX; 27 } 28 29 function getContainer() { 30 return document.querySelector('.chatroom-container'); 31 } 32 33 function getMainContent() { 34 return document.querySelector('.chatroom-container .main-content-area'); 35 } 36 37 function readStored() { 38 try { 39 const raw = localStorage.getItem(STORAGE_KEY); 40 if (!raw) return null; 41 const parsed = JSON.parse(raw); 42 if (!parsed || typeof parsed !== 'object') return null; 43 return parsed; 44 } catch (e) { 45 return null; 46 } 47 } 48 49 function writeStored(channels, members) { 50 try { 51 localStorage.setItem(STORAGE_KEY, JSON.stringify({ 52 channels: channels, 53 members: members 54 })); 55 } catch (e) { /* ignore */ } 56 } 57 58 function clamp(value, min, max) { 59 return Math.min(max, Math.max(min, value)); 60 } 61 62 function getMaxChannelsWidth(membersWidth) { 63 return Math.min(CHANNELS_MAX, window.innerWidth - NAV_WIDTH - membersWidth - CHAT_MIN); 64 } 65 66 function getMaxMembersWidth(channelsWidth) { 67 return Math.min(MEMBERS_MAX, window.innerWidth - NAV_WIDTH - channelsWidth - CHAT_MIN); 68 } 69 70 function applyWidths(channelsWidth, membersWidth) { 71 const container = getContainer(); 72 if (!container) return; 73 74 let channels = clamp(channelsWidth, CHANNELS_MIN, CHANNELS_MAX); 75 let members = clamp(membersWidth, MEMBERS_MIN, MEMBERS_MAX); 76 77 channels = clamp(channels, CHANNELS_MIN, getMaxChannelsWidth(members)); 78 members = clamp(members, MEMBERS_MIN, getMaxMembersWidth(channels)); 79 80 container.style.setProperty('--chatroom-channels-width', channels + 'px'); 81 container.style.setProperty('--chatroom-members-width', members + 'px'); 82 83 return { channels: channels, members: members }; 84 } 85 86 function loadAndApply() { 87 const stored = readStored(); 88 const channels = stored && Number(stored.channels) || DEFAULT_CHANNELS; 89 const members = stored && Number(stored.members) || DEFAULT_MEMBERS; 90 return applyWidths(channels, members); 91 } 92 93 function createHandle(target, label) { 94 const handle = document.createElement('div'); 95 handle.className = 'chatroom-resize-handle chatroom-resize-handle--' + target; 96 handle.setAttribute('role', 'separator'); 97 handle.setAttribute('aria-orientation', 'vertical'); 98 handle.setAttribute('aria-label', label); 99 handle.tabIndex = 0; 100 return handle; 101 } 102 103 function onPointerMove(clientX) { 104 if (!activeDrag) return; 105 const container = getContainer(); 106 if (!container) return; 107 108 const delta = clientX - activeDrag.startX; 109 let channels = activeDrag.startChannels; 110 let members = activeDrag.startMembers; 111 112 if (activeDrag.target === 'channels') { 113 channels = activeDrag.startChannels + delta; 114 } else if (activeDrag.target === 'members') { 115 members = activeDrag.startMembers - delta; 116 } 117 118 const applied = applyWidths(channels, members); 119 activeDrag.currentChannels = applied.channels; 120 activeDrag.currentMembers = applied.members; 121 } 122 123 function endDrag() { 124 if (!activeDrag) return; 125 126 if (activeDrag.handle) { 127 activeDrag.handle.classList.remove('is-active'); 128 } 129 document.body.classList.remove('chatroom-is-resizing'); 130 131 writeStored(activeDrag.currentChannels, activeDrag.currentMembers); 132 133 document.removeEventListener('mousemove', onMouseMove); 134 document.removeEventListener('mouseup', onMouseUp); 135 document.removeEventListener('touchmove', onTouchMove); 136 document.removeEventListener('touchend', onTouchEnd); 137 document.removeEventListener('touchcancel', onTouchEnd); 138 139 activeDrag = null; 140 } 141 142 function onMouseMove(e) { 143 e.preventDefault(); 144 onPointerMove(e.clientX); 145 } 146 147 function onMouseUp() {
148 endDrag(); 149 } 150 151 function onTouchMove(e) { 152 if (!activeDrag || !e.touches.length) return; 153 e.preventDefault(); 154 onPointerMove(e.touches[0].clientX); 155 } 156 157 function onTouchEnd() { 158 endDrag(); 159 } 160 161 function startDrag(target, handle, clientX) { 162 const container = getContainer(); 163 if (!container) return; 164 165 const channelsVar = getComputedStyle(container).getPropertyValue('--chatroom-channels-width').trim(); 166 const membersVar = getComputedStyle(container).getPropertyValue('--chatroom-members-width').trim(); 167 const startChannels = parseFloat(channelsVar) || DEFAULT_CHANNELS; 168 const startMembers = parseFloat(membersVar) || DEFAULT_MEMBERS; 169 170 activeDrag = { 171 target: target, 172 handle: handle, 173 startX: clientX, 174 startChannels: startChannels, 175 startMembers: startMembers, 176 currentChannels: startChannels, 177 currentMembers: startMembers 178 }; 179 180 handle.classList.add('is-active'); 181 document.body.classList.add('chatroom-is-resizing'); 182 183 document.addEventListener('mousemove', onMouseMove); 184 document.addEventListener('mouseup', onMouseUp); 185 document.addEventListener('touchmove', onTouchMove, { passive: false }); 186 document.addEventListener('touchend', onTouchEnd); 187 document.addEventListener('touchcancel', onTouchEnd); 188 } 189 190 function bindHandle(handle, target) { 191 handle.addEventListener('mousedown', function (e) { 192 if (e.button !== 0 || isMobile()) return; 193 e.preventDefault(); 194 startDrag(target, handle, e.clientX); 195 }); 196 197 handle.addEventListener('touchstart', function (e) { 198 if (isMobile() || !e.touches.length) return; 199 e.preventDefault(); 200 startDrag(target, handle, e.touches[0].clientX); 201 }, { passive: false }); 202 203 handle.addEventListener('keydown', function (e) { 204 if (isMobile()) return; 205 const step = e.shiftKey ? 24 : 8; 206 const container = getContainer(); 207 if (!container) return; 208 209 const channelsVar = getComputedStyle(container).getPropertyValue('--chatroom-channels-width').trim(); 210 const membersVar = getComputedStyle(container).getPropertyValue('--chatroom-members-width').trim(); 211 let channels = parseFloat(channelsVar) || DEFAULT_CHANNELS; 212 let members = parseFloat(membersVar) || DEFAULT_MEMBERS; 213 214 if (target === 'channels') { 215 if (e.key === 'ArrowLeft') channels -= step; 216 else if (e.key === 'ArrowRight') channels += step; 217 else return; 218 } else { 219 if (e.key === 'ArrowLeft') members += step; 220 else if (e.key === 'ArrowRight') members -= step; 221 else return; 222 } 223 224 e.preventDefault(); 225 const applied = applyWidths(channels, members); 226 writeStored(applied.channels, applied.members); 227 }); 228 } 229 230 function mountHandles() { 231 const container = getContainer(); 232 const mainContent = getMainContent(); 233 const channelsColumn = container && container.querySelector('.channels-column'); 234 const membersColumn = mainContent && mainContent.querySelector('.members-column'); 235 if (!container || !mainContent || !channelsColumn || !membersColumn) return false; 236 237 if (!channelsHandle) { 238 channelsHandle = createHandle('channels', 'Resize channel list'); 239 channelsColumn.insertAdjacentElement('afterend', channelsHandle); 240 bindHandle(channelsHandle, 'channels'); 241 } 242 243 if (!membersHandle) { 244 membersHandle = createHandle('members', 'Resize members list'); 245 membersColumn.insertAdjacentElement('beforebegin', membersHandle); 246 bindHandle(membersHandle, 'members'); 247 } 248 249 return true; 250 } 251 252 function unmountHandles() { 253 if (channelsHandle && channelsHandle.parentNode) { 254 channelsHandle.parentNode.removeChild(channelsHandle); 255 } 256 if (membersHandle && membersHandle.parentNode) { 257 membersHandle.parentNode.removeChild(membersHandle); 258 } 259 channelsHandle = null; 260 membersHandle = null; 261 } 262 263 function onWindowResize() { 264 if (isMobile()) { 265 if (booted) destroy(); 266 return; 267 } 268 if (!getContainer()) return; 269 if (!booted) { 270 init(); 271 return; 272 } 273 274 const container = getContainer(); 275 const channelsVar = getComputedStyle(container).getPropertyValue('--chatroom-channels-width').trim(); 276 const membersVar = getComputedStyle(container).getPropertyValue('--chatroom-members-width').trim(); 277 const channels = parseFloat(channelsVar) || DEFAULT_CHANNELS; 278 const members = parseFloat(membersVar) || DEFAULT_MEMBERS; 279 const applied = applyWidths(channels, members); 280 writeStored(applied.channels, applied.members); 281 } 282
283 function init() { 284 if (!getContainer()) return false; 285 if (isMobile()) return false; 286 287 loadAndApply(); 288 if (!mountHandles()) return false; 289 290 if (!booted) { 291 resizeListener = onWindowResize; 292 window.addEventListener('resize', resizeListener); 293 booted = true; 294 } 295 296 return true; 297 } 298 299 function destroy() { 300 endDrag(); 301 unmountHandles(); 302 if (resizeListener) { 303 window.removeEventListener('resize', resizeListener); 304 resizeListener = null; 305 } 306 booted = false; 307 } 308 309 function refresh() { 310 if (isMobile()) { 311 destroy(); 312 return false; 313 } 314 if (!booted) return init(); 315 loadAndApply(); 316 return true; 317 } 318 319 return { 320 init: init, 321 destroy: destroy, 322 refresh: refresh 323 }; 324})();
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.