PageSourceSearch

https://pupwife.com/projects/chatroom/scripts/chatroomResize.js

js pupwife.com collected 2026-09-30 15:07:24 UTC 10,803 bytes, 324 lines download raw bytes

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.