PageSourceSearch

https://certified-green-team-li.com/js/pop.up.lead.form.draggable.js

js certified-green-team-li.com collected 2026-09-27 05:47:26 UTC 8,132 bytes, 264 lines download raw bytes

1/**
2 * Avatar Draggable Module - Lazy-loaded only when avatarDraggable setting is enabled
3 * Allows mobile users to drag the avatar button to reposition it
4 */
5
6const POSITION_STORAGE_KEY = 'bubblePosition';
7const DRAG_THRESHOLD = 5; // pixels before considered a drag
8const POSITION_EXPIRATION_MS = 60 * 60 * 1000; // 1 hour
9
10/**
11 * Initialize draggable functionality on the avatar
12 * @param {Object} component - The UserAgentComponent instance
13 */
14export function initDraggable(component) {
15    const $handle = component.container.find('.user-agent-avatar-img-wrapper');
16    if (!$handle.length) return;
17
18    // Only enable dragging on mobile devices (small screen + touch capability)
19    const isMobile = window.innerWidth <= 768 && ('ontouchstart' in window || navigator.maxTouchPoints > 0);
20    if (!isMobile) return;
21
22    // Store handle reference on component for other methods
23    component.$handle = $handle;
24
25    // Wait for container to be visible before setting up position
26    waitForVisible(component, () => {
27        convertToLeftTopPositioning(component);
28        loadPosition(component);
29    });
30
31    // Set up event handlers
32    setupDragEvents(component);
33}
34
35/**
36 * Wait for container to be visible, then execute callback
37 */
38function waitForVisible(component, callback) {
39    if (component.container.is(':visible')) {
40        callback();
41    } else {
42        setTimeout(() => waitForVisible(component, callback), 100);
43    }
44}
45
46/**
47 * Convert from CSS bottom/right to left/top positioning for dragging
48 */
49function convertToLeftTopPositioning(component) {
50    const rect = component.container[0].getBoundingClientRect();
51    component.container.css({
52        left: rect.left + 'px',
53        top: rect.top + 'px',
54        right: 'auto',
55        bottom: 'auto'
56    });
57}
58
59/**
60 * Set up mouse and touch drag events
61 */
62function setupDragEvents(component) {
63    let startX, startY, startLeft, startTop, hasMoved;
64    let isTouchingAvatar = false;
65
66    const beginDragTracking = (clientX, clientY) => {
67        startX = clientX;
68        startY = clientY;
69        startLeft = parseInt(component.container.css('left')) || 0;
70        startTop = parseInt(component.container.css('top')) || 0;
71        hasMoved = false;
72    };
73
74    const onMove = (clientX, clientY, e) => {
75        const deltaX = clientX - startX;
76        const deltaY = clientY - startY;
77
78        // Only start dragging after moving past threshold
79        if (!hasMoved && (Math.abs(deltaX) > DRAG_THRESHOLD || Math.abs(deltaY) > DRAG_THRESHOLD)) {
80            hasMoved = true;
81            startDragging(component);
82            startLeft = parseInt(component.container.css('left')) || 0;
83        }
84
85        if (hasMoved) {
86            e.preventDefault();
87
88            let newLeft = startLeft + deltaX;
89            let newTop = startTop + deltaY;
90
91            const avatarRect = component.$handle[0].getBoundingClientRect();
92            const currentCssLeft = parseInt(component.container.css('left')) || 0;
93            const currentCssTop = parseInt(component.container.css('top')) || 0;
94
95            const minLeft = currentCssLeft - avatarRect.left;
96            const maxLeft = currentCssLeft + (window.innerWidth - avatarRect.right);
97            const minTop = currentCssTop - avatarRect.top;
98            const maxTop = currentCssTop + (window.innerHeight - avatarRect.bottom);
99
100            newLeft = Math.max(minLeft, Math.min(maxLeft, newLeft));
101            newTop = Math.max(minTop, Math.min(maxTop, newTop));
102
103            component.container.css({
104                left: newLeft + 'px',
105                top: newTop + 'px'
106            });
107        }
108    };
109
110    const onEnd = () => {
111        if (hasMoved) {
112            finishDragging(component);
113        }
114    };
115
116    // Mouse events
117    const onMouseDown = (e) => {
118        beginDragTracking(e.clientX, e.clientY);
119
120        const onMouseMove = (e) => onMove(e.clientX, e.clientY, e);
121        const onMouseUp = () => {
122            $(document).off('mousemove.drag mouseup.drag');
123            onEnd();
124        };
125
126        $(document).on('mousemove.drag', onMouseMove);
127        $(document).on('mouseup.drag', onMouseUp);
128    };
129
130    // Touch events
131    const onTouchStart = (e) => {
132        isTouchingAvatar = true;
133        const touch = e.touches[0];
134        beginDragTracking(touch.clientX, touch.clientY);
135    };
136
137    const onTouchMove = (e) => {
138        if (!isTouchingAvatar) return;
139        const touch = e.touches[0];
140        onMove(touch.clientX, touch.clientY, e);
141    };
142
143    const onTouchEnd = () => {
144        if (!isTouchingAvatar) return;
145        isTouchingAvatar = false;
146        onEnd();
147    };
148
149    // Attach to handle and img
150    const $img = component.$handle.find('img');
151    [component.$handle[0], $img[0]].forEach(el => {
152        if (el) {
153            el.addEventListener('mousedown', onMouseDown, true);
154            el.addEventListener('touchstart', onTouchStart, { passive: true, capture: true });
155        }
156    });
157    document.addEventListener('touchmove', onTouchMove, { passive: false });
158    document.addEventListener('touchend', onTouchEnd);
159}
160
161/**
162 * Called when drag movement starts (past threshold)
163 */
164function startDragging(component) {
165    component.isDragging = true;
166    component.container.addClass('dragging');
167
168    const avatarBefore = component.$handle[0].getBoundingClientRect();
169    component.setMessage("");
170
171    const avatarAfter = component.$handle[0].getBoundingClientRect();
172    const shiftX = avatarAfter.left - avatarBefore.left;
173    const shiftY = avatarAfter.top - avatarBefore.top;
174    if (shiftX !== 0) {
175        const currentLeft = parseInt(component.container.css('left')) || 0;
176        component.container.css('left', (currentLeft - shiftX) + 'px');
177    }
178    if (shiftY !== 0) {
179        const currentTop = parseInt(component.container.css('top')) || 0;
180        component.container.css('top', (currentTop - shiftY) + 'px');
181    }
182}
183
184/**
185 * Called when drag ends
186 */
187function finishDragging(component) {
188    component.isDragging = false;
189    component.container.removeClass('dragging');
190
191    savePosition(component);
192
193    component.justFinishedDragging = true;
194    setTimeout(() => {
195        component.justFinishedDragging = false;
196    }, 100);
197}
198
199/**
200 * Save current position to localStorage with timestamp
201 */
202function savePosition(component) {
203    const left = parseInt(component.container.css('left')) || 0;
204    const top = parseInt(component.container.css('top')) || 0;
205    const timestamp = Date.now();
206    localStorage.setItem(POSITION_STORAGE_KEY, JSON.stringify({ left, top, timestamp }));
207}
208
209/**
210 * Load saved position from localStorage if not expired
211 */
212function loadPosition(component) {
213    const saved = localStorage.getItem(POSITION_STORAGE_KEY);
214    if (!saved) return;
215
216    try {
217        const pos = JSON.parse(saved);
218
219        if (pos.timestamp && (Date.now() - pos.timestamp) > POSITION_EXPIRATION_MS) {
220            localStorage.removeItem(POSITION_STORAGE_KEY);
221            return;
222        }
223
224        component.setMessage("");
225
226        component.container.css({
227            left: pos.left + 'px',
228            top: pos.top + 'px'
229        });
230
231        checkViewportBounds(component);
232    } catch (e) {
233        // Invalid saved data, ignore
234    }
235}
236
237/**
238 * Check if avatar is within viewport, reset to default if not
239 */
240function checkViewportBounds(component) {
241    const $avatar = component.container.find('.user-agent-avatar-img-wrapper');
242    if (!$avatar.length) return;
243
244    const avatarRect = $avatar[0].getBoundingClientRect();
245    const isOffScreen = avatarRect.left < 0 ||
246                       avatarRect.right > window.innerWidth ||
247                       avatarRect.top < 0 ||
248                       avatarRect.bottom > window.innerHeight;
249
250    if (isOffScreen) {
251        resetToDefault(component);
252    }
253}
254
255/**
256 * Reset position to default (bottom-right corner)
257 */
258function resetToDefault(component) {
259    component.container.addClass('message-left');
260    component.container.css({ left: 'auto', top: 'auto', right: '0', bottom: '0' });
261
262    convertToLeftTopPositioning(component);
263    savePosition(component);
264}

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.