PageSourceSearch

https://pupwife.com/scripts/desktop/citrusDesktopSelection.js

js pupwife.com collected 2026-09-30 15:04:57 UTC 8,855 bytes, 268 lines download raw bytes

1/**
2 * CitrusOS desktop icon selection — marquee box, multi-select, keyboard helpers.
3 */
4(function (global) {
5    'use strict';
6
7    var MARQUEE_THRESHOLD = 6;
8    var desktopEl = null;
9    var marqueeEl = null;
10    /** @type {Record<string, boolean>} */
11    var selected = {};
12    var anchorId = null;
13
14    function getOrderedShortcutIds() {
15        if (!global.CitrusDesktopShortcuts) return [];
16        var list = global.CitrusDesktopShortcuts.listVisibleShortcuts().slice();
17        list.sort(function (a, b) {
18            if (a.y !== b.y) return a.y - b.y;
19            return a.x - b.x;
20        });
21        var ids = [];
22        for (var i = 0; i < list.length; i++) ids.push(list[i].id);
23        return ids;
24    }
25
26    function getSelectedIds() {
27        return Object.keys(selected);
28    }
29
30    function getSelectedCount() {
31        return Object.keys(selected).length;
32    }
33
34    function isSelected(id) {
35        return !!selected[id];
36    }
37
38    function syncDom() {
39        if (!desktopEl) return;
40        var icons = desktopEl.querySelectorAll('.citrus-desktop-icon');
41        for (var i = 0; i < icons.length; i++) {
42            var ic = icons[i];
43            var sid = ic.getAttribute('data-shortcut-id');
44            ic.classList.toggle('citrus-desktop-icon--selected', !!selected[sid]);
45        }
46    }
47
48    function clearSelection() {
49        selected = {};
50        anchorId = null;
51        syncDom();
52    }
53
54    function setSelection(ids, opts) {
55        opts = opts || {};
56        if (!opts.add) selected = {};
57        for (var i = 0; i < ids.length; i++) {
58            if (ids[i]) selected[ids[i]] = true;
59        }
60        if (ids.length && !opts.keepAnchor) {
61            anchorId = ids[0];
62        }
63        syncDom();
64    }
65
66    function selectOnly(id) {
67        selected = {};
68        if (id) {
69            selected[id] = true;
70            anchorId = id;
71        } else {
72            anchorId = null;
73        }
74        syncDom();
75    }
76
77    function toggleSelection(id) {
78        if (!id) return;
79        if (selected[id]) {
80            delete selected[id];
81            if (anchorId === id) anchorId = getSelectedIds()[0] || null;
82        } else {
83            selected[id] = true;
84            anchorId = id;
85        }
86        syncDom();
87    }
88
89    function selectRange(toId) {
90        if (!toId) return;
91        var ordered = getOrderedShortcutIds();
92        var fromId = anchorId || toId;
93        var fromIdx = ordered.indexOf(fromId);
94        var toIdx = ordered.indexOf(toId);
95        if (fromIdx === -1) fromIdx = toIdx;
96        if (toIdx === -1) return;
97        var lo = Math.min(fromIdx, toIdx);
98        var hi = Math.max(fromIdx, toIdx);
99        selected = {};
100        for (var i = lo; i <= hi; i++) selected[ordered[i]] = true;
101        anchorId = fromId;
102        syncDom();
103    }
104
105    function selectAll() {
106        setSelection(getOrderedShortcutIds());
107    }
108
109    function handleIconPointerDown(id, ev) {
110        if (ev.button !== 0 || !id) return;
111        if (ev.shiftKey) {
112            selectRange(id);
113            return;
114        }
115        if (ev.ctrlKey || ev.metaKey) {
116            toggleSelection(id);
117            return;
118        }
119        if (!isSelected(id) || getSelectedCount() === 1) {
120            selectOnly(id);
121        }
122    }
123
124    function rectsIntersect(a, b) {
125        return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
126    }
127
128    function iconsInMarquee(x1, y1, x2, y2) {
129        var selRect = {
130            left: Math.min(x1, x2),
131            top: Math.min(y1, y2),
132            right: Math.max(x1, x2),
133            bottom: Math.max(y1, y2)
134        };
135        var hits = [];
136        if (!desktopEl) return hits;
137        var icons = desktopEl.querySelectorAll('.citrus-desktop-icon');
138        for (var i = 0; i < icons.length; i++) {
139            var r = icons[i].getBoundingClientRect();
140            if (rectsIntersect(selRect, r)) {
141                hits.push(icons[i].getAttribute('data-shortcut-id'));
142            }
143        }
144        return hits;
145    }
146
147    function updateMarqueeRect(x1, y1, x2, y2) {
148        if (!marqueeEl || !desktopEl) return;
149        var deskRect = desktopEl.getBoundingClientRect();
150        var left = Math.min(x1, x2) - deskRect.left;
151        var top = Math.min(y1, y2) - deskRect.top;
152        var width = Math.abs(x2 - x1);
153        var height = Math.abs(y2 - y1);
154        marqueeEl.style.left = left + 'px';
155        marqueeEl.style.top = top + 'px';
156        marqueeEl.style.width = width + 'px';
157        marqueeEl.style.height = height + 'px';
158        marqueeEl.hidden = !(width > 0 && height > 0);
159    }
160
161    function ensureMarqueeEl() {
162        if (marqueeEl || !desktopEl) return;
163        marqueeEl = document.createElement('div');
164        marqueeEl.className = 'citrus-desktop-marquee';
165        marqueeEl.hidden = true;
166        marqueeEl.setAttribute('aria-hidden', 'true');
167        desktopEl.appendChild(marqueeEl);
168    }
169
170    function bindMarquee() {
171        if (!desktopEl) return;
172
173        desktopEl.addEventListener('pointerdown', function (ev) {
174            if (ev.button !== 0) return;
175            if (ev.target.closest('.citrus-desktop-icon')) return;
176            if (global.CitrusDesktopDnD && global.CitrusDesktopDnD.isDragActive &&
177                global.CitrusDesktopDnD.isDragActive()) {
178                return;
179            }
180
181            var startX = ev.clientX;
182            var startY = ev.clientY;
183            var moved = false;
184            var marqueeActive = false;
185            var addMode = ev.ctrlKey || ev.metaKey;
186            var baseSelection = addMode ? getSelectedIds().slice() : [];
187
188            function applyMarqueeHits(clientX, clientY) {
189                var hits = iconsInMarquee(startX, startY, clientX, clientY);
190                if (addMode) {
191                    var merged = baseSelection.slice();
192                    for (var i = 0; i < hits.length; i++) {
193                        if (merged.indexOf(hits[i]) === -1) merged.push(hits[i]);
194                    }
195                    setSelection(merged, { keepAnchor: true });
196                } else {
197                    setSelection(hits, { keepAnchor: true });
198                }
199                if (hits.length) anchorId = hits[0];
200            }
201
202            function onMove(e) {
203                var dx = e.clientX - startX;
204                var dy = e.clientY - startY;
205                if (!moved && (Math.abs(dx) > MARQUEE_THRESHOLD || Math.abs(dy) > MARQUEE_THRESHOLD)) {
206                    moved = true;
207                    marqueeActive = true;
208                    ensureMarqueeEl();
209                    if (!addMode) clearSelection();
210                    desktopEl.classList.add('citrusos-desktop--marquee-active');
211                    if (marqueeEl) marqueeEl.hidden = false;
212                    if (desktopEl.setPointerCapture) desktopEl.setPointerCapture(ev.pointerId);
213                }
214                if (!marqueeActive) return;
215                e.preventDefault();
216                updateMarqueeRect(startX, startY, e.clientX, e.clientY);
217                applyMarqueeHits(e.clientX, e.clientY);
218            }
219
220            function onUp(e) {
221                if (desktopEl.hasPointerCapture && desktopEl.hasPointerCapture(ev.pointerId)) {
222                    desktopEl.releasePointerCapture(ev.pointerId);
223                }
224                document.removeEventListener('pointermove', onMove);
225                document.removeEventListener('pointerup', onUp);
226                document.removeEventListener('pointercancel', onUp);
227                desktopEl.classList.remove('citrusos-desktop--marquee-active');
228                if (marqueeEl) marqueeEl.hidden = true;
229                if (!moved && !addMode) clearSelection();
230            }
231
232            document.addEventListener('pointermove', onMove);
233            document.addEventListener('pointerup', onUp);
234            document.addEventListener('pointercancel', onUp);
235        });
236    }
237
238    function init(el) {
239        desktopEl = el;
240        ensureMarqueeEl();
241        bindMarquee();
242    }
243
244    function restoreSelection(ids) {
245        selected = {};
246        for (var i = 0; i < ids.length; i++) {
247            if (ids[i]) selected[ids[i]] = true;
248        }
249        anchorId = ids[0] || null;
250        syncDom();
251    }
252
253    global.CitrusDesktopSelection = {
254        init: init,
255        clear: clearSelection,
256        selectOnly: selectOnly,
257        toggle: toggleSelection,
258        selectRange: selectRange,
259        selectAll: selectAll,
260        getSelectedIds: getSelectedIds,
261        getSelectedCount: getSelectedCount,
262        isSelected: isSelected,
263        syncDom: syncDom,
264        handleIconPointerDown: handleIconPointerDown,
265        restoreSelection: restoreSelection,
266        getAnchorId: function () { return anchorId; }
267    };
268})(typeof window !== 'undefined' ? window : this);

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.