/* * Browser and DOM utility functions for the RSpade framework. * These functions handle browser detection, viewport utilities, and DOM manipulation. */ // ============================================================================ // BROWSER DETECTION // ============================================================================ /** * Detects if user is on a mobile device or using mobile viewport * @returns {boolean} True if mobile device or viewport < 992px * @todo Improve user agent detection for all mobile devices */ function is_mobile() { if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { return true; } else if ($(window).width() < 992) { // 992px = bootstrap 4 col-md- return true; } else { return false; } } /** * Detects if user is on desktop (not mobile) * @returns {boolean} True if not mobile device/viewport */ function is_desktop() { return !is_mobile(); } /** * Detects the user's operating system * @returns {string} OS name: 'Mac OS', 'iPhone', 'iPad', 'Windows', 'Android-Phone', 'Android-Tablet', 'Linux', or 'Unknown' */ function get_os() { let user_agent = window.navigator.userAgent, platform = window.navigator.platform, macos_platforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], windows_platforms = ['Win32', 'Win64', 'Windows', 'WinCE'], ios_platforms = ['iPhone', 'iPad', 'iPod'], os = null; let is_mobile_device = is_mobile(); if (macos_platforms.indexOf(platform) !== -1) { os = 'Mac OS'; } else if (ios_platforms.indexOf(platform) !== -1 && is_mobile_device) { os = 'iPhone'; } else if (ios_platforms.indexOf(platform) !== -1 && !is_mobile_device) { os = 'iPad'; } else if (windows_platforms.indexOf(platform) !== -1) { os = 'Windows'; } else if (/Android/.test(user_agent) && is_mobile_device) { os = 'Android-Phone'; } else if (/Android/.test(user_agent) && !is_mobile_device) { os = 'Android-Tablet'; } else if (!os && /Linux/.test(platform)) { os = 'Linux'; } else { os = 'Unknown'; } return os; } /** * Detects if the user agent is a web crawler/bot * @returns {boolean} True if user agent appears to be a bot/crawler */ function is_crawler() { let user_agent = navigator.userAgent; let bot_pattern = /bot|spider|crawl|slurp|archiver|ping|search|dig|tracker|monitor|snoopy|yahoo|baidu|msn|ask|teoma|axios/i; return bot_pattern.test(user_agent); } // ============================================================================ // DOM SCROLLING UTILITIES // ============================================================================ /** * Scrolls parent container to make target element visible if needed * @param {string|HTMLElement|jQuery} target - Target element to scroll into view */ function scroll_into_view_if_needed(target) { const $target = $(target); // Find the closest parent with overflow-y: auto const $parent = $target.parent(); // Calculate the absolute top position of the target const target_top = $target.position().top + $parent.scrollTop(); const target_height = $target.outerHeight(); const parent_height = $parent.height(); const scroll_position = $parent.scrollTop(); // Check if the target is out of view if (target_top < scroll_position || target_top + target_height > scroll_position + parent_height) { Debugger.console_debug('UI', 'Scrolling!', target_top); // Calculate the new scroll position to center the target let new_scroll_position = target_top + target_height / 2 - parent_height / 2; // Limit the scroll position between 0 and the maximum scrollable height new_scroll_position = Math.max(0, Math.min(new_scroll_position, $parent[0].scrollHeight - parent_height)); // Scroll the parent to the new scroll position $parent.scrollTop(new_scroll_position); } } /** * Scrolls page to make target element visible if needed (with animation) * @param {string|HTMLElement|jQuery} target - Target element to scroll into view */ function scroll_page_into_view_if_needed(target) { const $target = $(target); // Calculate the absolute top position of the target relative to the document const target_top = $target.offset().top; const target_height = $target.outerHeight(); const window_height = $(window).height(); const window_scroll_position = $(window).scrollTop(); // Check if the target is out of view if (target_top < window_scroll_position || target_top + target_height > window_scroll_position + window_height) { Debugger.console_debug('UI', 'Scrolling!', target_top); // Calculate the new scroll position to center the target const new_scroll_position = target_top + target_height / 2 - window_height / 2; // Animate the scroll to the new position $('html, body').animate( { scrollTop: new_scroll_position, }, 1000 ); // duration of the scroll animation in milliseconds } } // ============================================================================ // DOM UTILITIES // ============================================================================ /** * Waits for all images on the page to load * @param {Function} callback - Function to call when all images are loaded */ function wait_for_images(callback) { const $images = $('img'); // Get all img tags const total_images = $images.length; let images_loaded = 0; if (total_images === 0) { callback(); // if there are no images, immediately call the callback } $images.each(function () { const img = new Image(); img.onload = function () { images_loaded++; if (images_loaded === total_images) { callback(); // call the callback when all images are loaded } }; img.onerror = function () { images_loaded++; if (images_loaded === total_images) { callback(); // also call the callback if an image fails to load } }; img.src = this.src; // this triggers the loading }); } /** * Creates a jQuery element containing a non-breaking space * @returns {jQuery} jQuery span element with   */ function $nbsp() { return $(' '); } /** * Escapes special characters in a jQuery selector * @param {string} id - Element ID to escape * @returns {string} jQuery selector string with escaped special characters * @warning Not safe for security-critical operations */ function escape_jq_selector(id) { return '#' + id.replace(/(:|\.|\[|\]|,|=|@)/g, '\\$1'); }