/** * Webring Widget * Add this to your site to participate in the webring! * https://github.com/mocha/webring for more info */ (function() { // Default configuration const config = { ringlet: null, position: "bottom", // Possible values: top, bottom, left, right, top-left, top-right, bottom-left, bottom-right position_tab: "left", // Possible values: left, center, right siteUrl: null, // Allow users to specify their URL if auto-detection fails baseUrl: "https://webring.fun", // Base URL for the webring site type: "bar", // Widget type: "bar", "box", or "static" color: null, // Custom hex color code opacity: 1, // Opacity value from 0.2 to 1 slide_toggle: false, // Whether widget should have toggle tab and slide in/out full_width: false, // For static widget: whether it should take full width extra_details: true, // For static widget: whether to show detailed info auto_hide: false, // Whether widget should auto-hide after a delay hide_delay: 3000, // Delay in milliseconds before auto-hide static_position: null, // For static widget: position fixed_position: false, // For static widget: whether it's fixed-positioned start_collapsed: false, // For box widget: whether to start in collapsed (badge) mode tab_content: null, // Custom tab content widget_content: null // Custom widget content }; // Initialize with user options window.webring = { init: function(options) { console.log('DEBUG: Initializing webring widget with options:', options); // Merge user options with defaults Object.assign(config, options); console.log('DEBUG: Configuration after merging with defaults:', config); // Validate config values validateConfig(); console.log('DEBUG: Configuration after validation:', config); // Start loading the webring data loadWebringData(); } }; /** * Validates and normalizes the configuration */ function validateConfig() { // Set defaults for any unspecified options const defaults = { type: 'bar', position: 'bottom', position_tab: 'left', color: null, opacity: 0.85, siteUrl: null, ringlet: null, slide_toggle: false, full_width: false, extra_details: true, auto_hide: false, hide_delay: 3000, static_position: 'inline', fixed_position: false, start_collapsed: false }; // Apply defaults for any unspecified options for (const [key, value] of Object.entries(defaults)) { if (config[key] === undefined) { config[key] = value; } } // Validate type if (!['bar', 'box', 'static'].includes(config.type)) { console.warn(`Invalid widget type "${config.type}", defaulting to "bar"`); config.type = 'bar'; } // Validate position based on type if (config.type === 'bar' && !['top', 'bottom'].includes(config.position)) { console.warn(`Invalid position "${config.position}" for bar widget, defaulting to "bottom"`); config.position = 'bottom'; } else if (config.type === 'box' && !['top-left', 'top-right', 'bottom-left', 'bottom-right'].includes(config.position)) { console.warn(`Invalid position "${config.position}" for box widget, defaulting to "bottom-right"`); config.position = 'bottom-right'; } else if (config.type === 'static') { // These options are deprecated for static widget if (config.fixed_position || config.static_position !== 'inline') { console.warn('The "fixed_position" and "static_position" options are deprecated for the static widget type. The widget will now appear where the script is placed in your HTML.'); } } // Validate tab position if (!['left', 'center', 'right'].includes(config.position_tab)) { console.warn(`Invalid tab position "${config.position_tab}", defaulting to "left"`); config.position_tab = 'left'; } // Validate color only if it's provided if (config.color !== null && (typeof config.color !== 'string' || !config.color.match(/^#[0-9A-Fa-f]{6}$/))) { console.warn(`Invalid color "${config.color}", defaulting to "#000000"`); config.color = "#000000"; } // Validate opacity if (typeof config.opacity !== 'number' || config.opacity < 0 || config.opacity > 1) { console.warn(`Invalid opacity "${config.opacity}", defaulting to 0.85`); config.opacity = 0.85; } return config; } /** * Get icon HTML for navigation buttons */ function getIconHtml(iconType) { switch (iconType) { case 'left': return ''; case 'random': return ''; case 'right': return ''; default: return ''; } } // Color utility functions /** * Check if a color is light or dark */ function isColorLight(hexColor) { // Convert hex to RGB const r = parseInt(hexColor.slice(1, 3), 16); const g = parseInt(hexColor.slice(3, 5), 16); const b = parseInt(hexColor.slice(5, 7), 16); // Calculate luminance // Formula: 0.299*R + 0.587*G + 0.114*B const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; // Consider light if luminance is greater than 0.5 return luminance > 0.5; } /** * Darken a color for hover states */ function darkenColor(hexColor) { // Convert hex to RGB let r = parseInt(hexColor.slice(1, 3), 16); let g = parseInt(hexColor.slice(3, 5), 16); let b = parseInt(hexColor.slice(5, 7), 16); // Darken by 10% r = Math.max(0, Math.floor(r * 0.9)); g = Math.max(0, Math.floor(g * 0.9)); b = Math.max(0, Math.floor(b * 0.9)); // Convert back to hex return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; } /** * Get appropriate text color (black or white) for a background color */ function getTextColorForBackground(hexColor) { return isColorLight(hexColor) ? "black" : "white"; } /** * Load the appropriate data file based on configuration */ function loadWebringData() { // Determine which data file to load const dataUrl = config.ringlet ? `${config.baseUrl}/data/${config.ringlet}.json` : `${config.baseUrl}/data/all.json`; console.log(`Loading webring data from: ${dataUrl}`); console.log(`Config: ringlet=${config.ringlet}, baseUrl=${config.baseUrl}`); fetch(dataUrl) .then(response => { if (!response.ok) { throw new Error(`Failed to load webring data (${response.status})`); } return response.json(); }) .then(data => { console.log('Webring data loaded:', data); renderWidget(data); }) .catch(error => { console.error('Webring widget error:', error); // Optionally render a fallback or error message }); } /** * Find the current site in the ring and render the widget */ function renderWidget(data) { // Get current site URL, stripping trailing slash for consistency const currentUrl = detectCurrentSiteUrl(); console.log(`Current URL detected as: ${currentUrl}`); // Process the data based on structure let websites = []; let ringletName = null; let ringletDescription = null; let ringletUrl = null; let currentSiteColor = "#666666"; // Default color if none is found // Debug the structure of the data console.log('Data structure received:', { hasWebsites: !!data.websites, hasId: !!data.id, hasName: !!data.name, hasRinglets: !!data.ringlets, dataType: typeof data, keysInData: Object.keys(data) }); // Handle different data formats if (data.websites) { // This could be either full ring format or individual ringlet with metadata // Check if this is a ringlet file with metadata (has id, name fields) if (data.id && data.name) { // This is a ringlet file with metadata console.log(`Processing ringlet file with metadata: id=${data.id}, name=${data.name}`); websites = Object.values(data.websites); ringletName = data.name; ringletDescription = data.description; ringletUrl = data.url; console.log(`Set ringlet metadata: name=${ringletName}, url=${ringletUrl}`); } // Otherwise it's the full ring data else { console.log('Processing full ring data'); websites = Object.values(data.websites); // If a ringlet is specified in full ring data if (config.ringlet && data.ringlets && data.ringlets[config.ringlet]) { ringletName = data.ringlets[config.ringlet].name; ringletDescription = data.ringlets[config.ringlet].description; ringletUrl = data.ringlets[config.ringlet].url; console.log(`Using ringlet from full ring: ${config.ringlet}, name=${ringletName}, url=${ringletUrl}`); websites = websites.filter(site => site.ringlets && site.ringlets.includes(config.ringlet) ); } } } else { // Legacy format (flat object of websites) console.log('Processing legacy format (flat object)'); websites = Object.values(data); // Try to find ringlet name from the first site's ringlets info if (websites.length > 0 && websites[0].ringlets) { const ringletId = websites[0].ringlets[0]; ringletName = ringletId.replace(/_/g, ' '); // Fallback formatting console.log(`Using fallback ringlet name: ${ringletName}`); } } // Log what we determined for debugging console.log(`Final ringlet values: name=${ringletName}, url=${ringletUrl}, websites count=${websites.length}`); // Find current site in the websites array let currentSiteIndex = websites.findIndex(site => site.url.replace(/\/$/, '') === currentUrl.replace(/\/$/, '') ); // If site not found, pick a random position instead of showing error // This allows for local testing and graceful handling of misconfiguration if (currentSiteIndex === -1) { console.warn(`Webring widget: This site (${currentUrl}) is not in the ${ringletName || 'webring'}. Using fallback mode.`); // Use a random position for better testing/fallback experience currentSiteIndex = Math.floor(Math.random() * websites.length); } else { // Get the color of the current site if available if (websites[currentSiteIndex].color) { currentSiteColor = websites[currentSiteIndex].color; console.log(`Using site color: ${currentSiteColor}`); } } // If a custom color is provided, use it if (config.color) { currentSiteColor = config.color; console.log(`Using custom color: ${currentSiteColor}`); } // Calculate previous and next sites const prevSite = currentSiteIndex > 0 ? websites[currentSiteIndex - 1] : websites[websites.length - 1]; const nextSite = currentSiteIndex < websites.length - 1 ? websites[currentSiteIndex + 1] : websites[0]; // Create and insert the widget based on type if (config.type === "bar") { createBarWidgetDOM(prevSite, nextSite, websites, { name: ringletName, description: ringletDescription, url: ringletUrl }, currentSiteColor); } else if (config.type === "box") { createBoxWidgetDOM(prevSite, nextSite, websites, { name: ringletName, description: ringletDescription, url: ringletUrl }, currentSiteColor); } else if (config.type === "static") { createStaticWidgetDOM(prevSite, nextSite, websites, { name: ringletName, description: ringletDescription, url: ringletUrl }, currentSiteColor); } } /** * Tries to detect the current site URL * Falls back to user-provided URL if available */ function detectCurrentSiteUrl() { // If the user specified a URL, use that if (config.siteUrl) { return config.siteUrl; } // Otherwise try to get it from the current page return window.location.origin; } /** * Creates and inserts the bar widget DOM elements */ function createBarWidgetDOM(prevSite, nextSite, allSites, ringlet, siteColor) { // Create container const container = document.createElement('div'); container.id = 'webring-widget'; container.className = `webring-widget webring-widget-bar`; // Debug the ringlet object being passed to createWidgetDOM console.log('Creating bar widget DOM with ringlet:', ringlet); // Calculate a random site that's not the current one const getRandomSite = () => { // Get current URL const currentUrl = detectCurrentSiteUrl(); // Filter out the current site const otherSites = allSites.filter(site => site.url.replace(/\/$/, '') !== currentUrl.replace(/\/$/, '') ); // Pick a random site return otherSites[Math.floor(Math.random() * otherSites.length)]; }; // Prepare random site const randomSite = getRandomSite(); // Handle ringlet URL and text according to requirements: let ringletLinkUrl; let ringletDisplayText; if (!ringlet.name) { // Case 1: No ringlet specified ringletLinkUrl = config.baseUrl; ringletDisplayText = 'webring.fun'; } else if (ringlet.name && !ringlet.url) { // Case 2: Ringlet specified but no URL ringletLinkUrl = `${config.baseUrl}/?ringlet=${config.ringlet}`; ringletDisplayText = `${ringlet.name} webring`; } else { // Case 3: Ringlet specified with URL ringletLinkUrl = ringlet.url; ringletDisplayText = `${ringlet.name} webring`; } console.log(`Widget display values: ringletLinkUrl=${ringletLinkUrl}, ringletDisplayText=${ringletDisplayText}`); // Determine text color based on background color const textColor = getTextColorForBackground(siteColor); // Create pull tab HTML if slide_toggle is enabled const pullTabHTML = config.slide_toggle ? `
` : ''; // Create the widget description content const widgetDescription = config.widget_content ? config.widget_content : `This site is a member of ${ringlet.name ? 'the ' : ''}${ringletDisplayText}!`; // Create HTML structure for the main content const contentHTML = ` `; // Set the container's innerHTML container.innerHTML = contentHTML; // Add styles with dynamic colors addBarWidgetStyles(siteColor, textColor); // Add to the document document.body.appendChild(container); // Check for slide toggle and saved state if (config.slide_toggle) { const savedState = sessionStorage.getItem('webring-bar-widget-state'); if (savedState === 'hidden') { // Don't make the widget visible if we've saved a hidden state container.classList.remove('webring-widget-visible'); } else { // Make the widget visible initially on first visit or if saved as visible container.classList.add('webring-widget-visible'); } setupSlideOut(container); } else { // If not using slide toggle, always make the widget visible container.classList.add('webring-widget-visible'); } // Add space to the page if widget is at the top if (config.position === 'top') { adjustPageForTopWidget(container); } } /** * Adds the necessary CSS for the bar widget */ function addBarWidgetStyles(backgroundColor, textColor) { // Check if styles are already added if (document.getElementById('webring-styles')) { return; } // Calculate hover background based on text color const hoverBackground = textColor === "black" ? "rgba(0, 0, 0, 0.1)" : "rgba(255, 255, 255, 0.1)"; // Create style element const styleEl = document.createElement('style'); styleEl.id = 'webring-styles'; // Define CSS for the widget let widgetStyles = ` .webring-widget-bar { position: fixed; left: 0; right: 0; ${config.position === 'top' ? 'top: 0; border-bottom: 1px solid' : 'bottom: 0; border-top: 1px solid'}; z-index: 9999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.5; background-color: ${backgroundColor}; opacity: ${config.opacity}; color: ${textColor}; border-color: ${textColor === "black" ? "rgba(0, 0, 0, 0.1)" : "rgba(255, 255, 255, 0.2)"}; transition: transform 0.3s ease, opacity 0.3s ease; transform: translateY(0); /* Visible by default */ } .webring-widget-bar.webring-widget-visible { transform: translateY(0); } .webring-widget-bar:not(.webring-widget-visible) { transform: ${config.position === 'top' ? 'translateY(-100%)' : 'translateY(100%)'}; } .webring-widget-bar:hover { opacity: 1; } .webring-widget-container { max-width: 1200px; margin: 0 auto; position: relative; width: 100%; } .webring-widget-content { max-width: 1200px; margin: 0 auto; padding: 0 1rem; display: flex; justify-content: space-between; align-items: center; height: 40px; position: relative; } .webring-widget a { color: inherit; text-decoration: underline; } `; // Add styles for the pull tab only if slide_toggle is enabled if (config.slide_toggle) { // Calculate the position for the tab based on config.position_tab let tabPositionCSS = ''; if (config.position_tab === 'left') { // Align with content padding (1rem = 16px) tabPositionCSS = 'left: 1rem;'; } else if (config.position_tab === 'right') { // Align with content at right tabPositionCSS = 'right: 1rem; left: auto;'; } else if (config.position_tab === 'center') { // Center the tab tabPositionCSS = 'left: 50%; transform: translateX(-50%);'; } widgetStyles += ` .webring-widget-pull-tab { position: absolute; ${config.position === 'top' ? 'bottom: -32px; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px;' : 'top: -32px; border-top-left-radius: 8px; border-top-right-radius: 8px;'}; ${tabPositionCSS} background-color: ${backgroundColor}; border: 1px solid ${textColor === "black" ? "rgba(0, 0, 0, 0.1)" : "rgba(255, 255, 255, 0.2)"}; ${config.position === 'top' ? 'border-top: none;' : 'border-bottom: none;'}; min-width: 100px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: opacity 0.3s ease; z-index: 9998; /* Just below the main widget z-index */ padding: 0 12px; } .webring-widget-pull-tab:hover { opacity: 1; } .webring-widget-pull-tab-icon { font-size: 14px; line-height: 1; display: flex; align-items: center; justify-content: center; gap: 8px; } .webring-widget-pull-tab-text { font-weight: bold; font-size: 14px; } `; } // Add styles for buttons, descriptions, etc. widgetStyles += ` .webring-widget-description a:hover { text-decoration: underline; } .webring-widget-button { display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; margin-left: 5px; border-radius: 50%; transition: background-color 0.2s; } .webring-widget-button:hover { background-color: ${hoverBackground}; } .webring-widget-button svg { width: 14px; height: 14px; fill: currentColor; } .webring-sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; } `; // Set the style content styleEl.textContent = widgetStyles; // Add styles to the document document.head.appendChild(styleEl); } /** * Creates and inserts the box widget DOM elements */ function createBoxWidgetDOM(prevSite, nextSite, allSites, ringlet, siteColor) { // Create container const container = document.createElement('div'); container.id = 'webring-widget'; container.className = `webring-widget webring-widget-box webring-widget-${config.position}`; // Debug the ringlet object being passed to createWidgetDOM console.log('Creating box widget DOM with ringlet:', ringlet); // Calculate a random site that's not the current one const getRandomSite = () => { // Get current URL const currentUrl = detectCurrentSiteUrl(); // Filter out the current site const otherSites = allSites.filter(site => site.url.replace(/\/$/, '') !== currentUrl.replace(/\/$/, '') ); // Pick a random site return otherSites[Math.floor(Math.random() * otherSites.length)]; }; // Prepare random site const randomSite = getRandomSite(); // Handle ringlet URL and text according to requirements: let ringletLinkUrl; let ringletDisplayText; if (!ringlet.name) { // Case 1: No ringlet specified ringletLinkUrl = config.baseUrl; ringletDisplayText = 'webring.fun'; } else if (ringlet.name && !ringlet.url) { // Case 2: Ringlet specified but no URL ringletLinkUrl = `${config.baseUrl}/?ringlet=${config.ringlet}`; ringletDisplayText = `${ringlet.name} webring`; } else { // Case 3: Ringlet specified with URL ringletLinkUrl = ringlet.url; ringletDisplayText = `${ringlet.name} webring`; } console.log(`Widget display values: ringletLinkUrl=${ringletLinkUrl}, ringletDisplayText=${ringletDisplayText}`); // Determine text color based on background color const textColor = getTextColorForBackground(siteColor); // Create reopen tab HTML - now using custom tab content if provided const reopenTabHTML = ` `; // Create tooltip HTML for each site const prevSiteTooltip = ` `; const nextSiteTooltip = ` `; const randomSiteTooltip = ` `; // Create a wrapper for both the widget and the reopen tab const wrapper = document.createElement('div'); wrapper.className = 'webring-widget-wrapper'; // Store the original position as a class on the wrapper wrapper.classList.add(`webring-widget-${config.position}`); // Create the widget description content const widgetDescription = config.widget_content ? config.widget_content : `This site is a member of ${ringlet.name ? 'the ' : ''}${ringletDisplayText}!`; // Create HTML structure for the main content with header container.innerHTML = ` `; // Create the reopen tab element const reopenTab = document.createElement('div'); reopenTab.className = 'webring-widget-reopen-container'; reopenTab.innerHTML = reopenTabHTML; // Add both to the wrapper wrapper.appendChild(container); wrapper.appendChild(reopenTab); // Add styles with dynamic colors addBoxWidgetStyles(siteColor, textColor); // Add to the document document.body.appendChild(wrapper); // Always make the widget visible initially container.classList.add('webring-widget-visible'); // Store the original position values for restoration when closed let originalPosition = { position: 'fixed', top: '', right: '', bottom: '', left: '' }; // Set initial position based on config.position if (config.position === 'top-right') { originalPosition.top = '20px'; originalPosition.right = '20px'; } else if (config.position === 'top-left') { originalPosition.top = '20px'; originalPosition.left = '20px'; } else if (config.position === 'bottom-left') { originalPosition.bottom = '20px'; originalPosition.left = '20px'; } else { // bottom-right is default originalPosition.bottom = '20px'; originalPosition.right = '20px'; } // Store last known dragged position let draggedPosition = { position: 'fixed', top: originalPosition.top, right: originalPosition.right, bottom: originalPosition.bottom, left: originalPosition.left }; // Set up close button functionality const closeButton = container.querySelector('.webring-widget-close-button'); if (closeButton) { closeButton.addEventListener('click', () => { // Save current position before changing it if (wrapper.style.top) draggedPosition.top = wrapper.style.top; if (wrapper.style.right) draggedPosition.right = wrapper.style.right; if (wrapper.style.bottom) draggedPosition.bottom = wrapper.style.bottom; if (wrapper.style.left) draggedPosition.left = wrapper.style.left; // Reset to original position wrapper.style.position = originalPosition.position; wrapper.style.top = originalPosition.top; wrapper.style.right = originalPosition.right; wrapper.style.bottom = originalPosition.bottom; wrapper.style.left = originalPosition.left; // Add classes for closed state wrapper.classList.add('webring-widget-closed'); wrapper.classList.add('webring-widget-tab-visible'); // Store closed state in session storage try { sessionStorage.setItem('webring-widget-closed', 'true'); // Store dragged position sessionStorage.setItem('webring-widget-position', JSON.stringify(draggedPosition)); } catch (e) { console.error('Failed to store widget state:', e); } }); } // Set up reopen tab functionality reopenTab.addEventListener('click', () => { wrapper.classList.remove('webring-widget-closed'); wrapper.classList.remove('webring-widget-tab-visible'); // Try to restore dragged position try { const savedPosition = JSON.parse(sessionStorage.getItem('webring-widget-position')); if (savedPosition) { wrapper.style.position = savedPosition.position; wrapper.style.top = savedPosition.top; wrapper.style.right = savedPosition.right; wrapper.style.bottom = savedPosition.bottom; wrapper.style.left = savedPosition.left; } } catch (e) { console.error('Failed to restore widget position:', e); } // Update session storage try { sessionStorage.removeItem('webring-widget-closed'); } catch (e) { console.error('Failed to update widget state:', e); } }); // Check if widget was previously closed in this session or configured to start collapsed try { if (sessionStorage.getItem('webring-widget-closed') === 'true' || config.start_collapsed) { wrapper.classList.add('webring-widget-closed'); wrapper.classList.add('webring-widget-tab-visible'); // Ensure we're in the original position wrapper.style.position = originalPosition.position; wrapper.style.top = originalPosition.top; wrapper.style.right = originalPosition.right; wrapper.style.bottom = originalPosition.bottom; wrapper.style.left = originalPosition.left; // If this is initial load with start_collapsed, store the state if (config.start_collapsed && sessionStorage.getItem('webring-widget-closed') !== 'true') { sessionStorage.setItem('webring-widget-closed', 'true'); } } else { // Check if we have a saved position to restore const savedPosition = JSON.parse(sessionStorage.getItem('webring-widget-position')); if (savedPosition) { wrapper.style.position = savedPosition.position; wrapper.style.top = savedPosition.top; wrapper.style.right = savedPosition.right; wrapper.style.bottom = savedPosition.bottom; wrapper.style.left = savedPosition.left; } } } catch (e) { console.error('Failed to retrieve widget state:', e); } // Add drag functionality to the box widget makeWidgetDraggable(container, wrapper, draggedPosition); } /** * Makes the box widget draggable */ function makeWidgetDraggable(widget, wrapper, draggedPosition) { const handle = widget.querySelector('.webring-widget-header'); if (!handle) return; let isDragging = false; let offsetX, offsetY; handle.addEventListener('mousedown', startDrag); handle.addEventListener('touchstart', startDrag, { passive: false }); function startDrag(e) { // Don't start dragging if the close button was clicked if (e.target.closest('.webring-widget-close-button')) { return; } e.preventDefault(); isDragging = true; // Get either mouse or touch coordinates const pageX = e.pageX || e.touches[0].pageX; const pageY = e.pageY || e.touches[0].pageY; // Calculate the offset const rect = widget.getBoundingClientRect(); offsetX = pageX - (rect.left + window.scrollX); offsetY = pageY - (rect.top + window.scrollY); // Add event listeners for moving and stopping document.addEventListener('mousemove', dragMove); document.addEventListener('touchmove', dragMove, { passive: false }); document.addEventListener('mouseup', stopDrag); document.addEventListener('touchend', stopDrag); // Add dragging class widget.classList.add('webring-widget-dragging'); } function dragMove(e) { if (!isDragging) return; e.preventDefault(); // Get either mouse or touch coordinates const pageX = e.pageX || e.touches[0].pageX; const pageY = e.pageY || e.touches[0].pageY; // Calculate new position let left = pageX - offsetX; let top = pageY - offsetY; // Get widget dimensions const rect = widget.getBoundingClientRect(); // Constrain to window boundaries left = Math.max(0, Math.min(left, window.innerWidth - rect.width)); top = Math.max(0, Math.min(top, window.innerHeight - rect.height)); // Apply new position - using fixed positioning to maintain position relative to viewport wrapper.style.position = 'fixed'; wrapper.style.left = `${left}px`; wrapper.style.top = `${top}px`; wrapper.style.bottom = 'auto'; // Ensure bottom is not also set when dragging (fixes the stretching issue) wrapper.style.right = 'auto'; // Ensure right is not also set when dragging // Update the saved position if (draggedPosition) { draggedPosition.position = 'fixed'; draggedPosition.top = `${top}px`; draggedPosition.left = `${left}px`; draggedPosition.right = 'auto'; draggedPosition.bottom = 'auto'; } // Update sessionStorage immediately try { sessionStorage.setItem('webring-widget-position', JSON.stringify(draggedPosition)); } catch (e) { console.error('Failed to save widget position:', e); } } function stopDrag() { if (!isDragging) return; isDragging = false; // Remove event listeners document.removeEventListener('mousemove', dragMove); document.removeEventListener('touchmove', dragMove); document.removeEventListener('mouseup', stopDrag); document.removeEventListener('touchend', stopDrag); // Remove dragging class widget.classList.remove('webring-widget-dragging'); } } /** * Adds the necessary CSS for the box widget */ function addBoxWidgetStyles(backgroundColor, textColor) { // Check if styles are already added if (document.getElementById('webring-styles')) { return; } // Calculate hover background based on text color const hoverBackground = textColor === "black" ? "rgba(0, 0, 0, 0.1)" : "rgba(255, 255, 255, 0.1)"; // Create style element const styleEl = document.createElement('style'); styleEl.id = 'webring-styles'; // Define CSS let widgetStyles = ` .webring-widget-wrapper { position: fixed; z-index: 9999; } .webring-widget-top-right { top: 20px; right: 20px; } .webring-widget-top-left { top: 20px; left: 20px; } .webring-widget-bottom-left { bottom: 20px; left: 20px; } .webring-widget-bottom-right { bottom: 20px; right: 20px; } .webring-widget-box { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.5; background-color: ${backgroundColor}; color: ${textColor}; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); width: 220px; opacity: ${config.opacity}; transition: opacity 0.3s ease, transform 0.3s ease; transform: translateY(0); overflow: visible; } .webring-widget-closed .webring-widget-box { display: none; } .webring-widget-reopen-container { display: none; } .webring-widget-tab-visible .webring-widget-reopen-container { display: block; } .webring-widget-reopen-tab { background-color: rgba(0, 0, 0, 0.9); color: white; border-radius: 8px; padding: 8px; margin-top: 10px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background-color 0.2s, opacity 0.3s; opacity: ${config.opacity}; } .webring-widget-reopen-tab:hover { opacity: 1; background-color: ${backgroundColor}; } .webring-widget-reopen-tab-icon { display: flex; align-items: center; gap: 8px; } .webring-widget-reopen-tab-text { font-weight: bold; font-size: 14px; } .webring-widget-header { display: flex; justify-content: space-between; align-items: center; padding: 5px 8px; background-color: ${textColor === "black" ? "rgba(0, 0, 0, 0.05)" : "rgba(255, 255, 255, 0.1)"}; cursor: move; user-select: none; } .webring-widget-drag-handle { display: flex; align-items: center; } .webring-widget-close-button { display: flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: 50%; cursor: pointer; opacity: 0.7; transition: opacity 0.2s, background-color 0.2s; } .webring-widget-close-button:hover { opacity: 1; background-color: ${textColor === "black" ? "rgba(0, 0, 0, 0.1)" : "rgba(255, 255, 255, 0.2)"}; } .webring-widget-box.webring-widget-visible { transform: translateY(0); opacity: ${config.opacity}; } .webring-widget-box:not(.webring-widget-visible) { transform: translateY(10px); opacity: 0; } .webring-widget-box:hover { opacity: 1; } .webring-widget-box-content { padding: 0; } .webring-widget-description { display: block; margin: 10px; text-align: center; } .webring-widget-dragging { opacity: 0.8; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); transition: none; } .webring-widget a { color: inherit; text-decoration: underline; } .webring-widget-description a:hover { text-decoration: underline; } .webring-widget-nav { display: flex; justify-content: space-between; margin: 8px; } .webring-widget-button-container { display: flex; flex-direction: column; align-items: center; text-decoration: none; padding: 4px; border-radius: 8px; transition: background-color 0.2s; width: 65px; /* Fixed width to ensure all buttons are equal size */ position: relative; } .webring-widget-button-container:hover { background-color: var(--hover-color); opacity: 0.85; } .webring-widget-button { display: flex; align-items: center; justify-content: center; width: 16px; height: 16px; border-radius: 50%; background: transparent; } .webring-widget-button svg { width: 12px; height: 12px; fill: currentColor; } .webring-widget-button-label { font-size: 11px; margin-top: 4px; text-align: center; width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* Tooltip styles */ .webring-widget-tooltip { position: absolute; top: -80px; left: 50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.9); color: white; border: none; border-radius: 4px; padding: 8px; width: 160px; font-size: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); opacity: 0; visibility: hidden; transition: opacity 0.2s, visibility 0.2s; pointer-events: none; z-index: 10000; text-align: center; } .webring-widget-tooltip:after { content: ''; position: absolute; bottom: -6px; left: 50%; transform: translateX(-50%); width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 6px solid rgba(0, 0, 0, 0.9); } .webring-widget-tooltip-title { font-weight: bold; margin-bottom: 4px; color: white; text-align: center; } .webring-widget-tooltip-description { font-size: 11px; opacity: 0.9; line-height: 1.3; max-height: 50px; overflow: hidden; text-overflow: ellipsis; color: rgba(255, 255, 255, 0.9); text-align: center; } .webring-widget-button-container:hover .webring-widget-tooltip { opacity: 1; visibility: visible; } .webring-sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; } `; // Add media query for mobile widgetStyles += ` @media (max-width: 600px) { .webring-widget-box { width: 180px; font-size: 12px; } .webring-widget-description { font-size: 12px; } .webring-widget-button { width: 16px; height: 16px; } .webring-widget-tooltip { width: 140px; } } `; // Set the style content styleEl.textContent = widgetStyles; // Add to document head document.head.appendChild(styleEl); } /** * Creates and inserts the static widget DOM elements */ function createStaticWidgetDOM(prevSite, nextSite, allSites, ringlet, siteColor) { // Create container const container = document.createElement('div'); container.id = 'webring-widget'; container.className = `webring-widget webring-widget-static ${config.full_width ? 'webring-widget-full-width' : ''}`; // Determine text color based on background color const textColor = getTextColorForBackground(siteColor); // Calculate a random site that's not the current one const getRandomSite = () => { // Get current URL const currentUrl = detectCurrentSiteUrl(); // Filter out the current site const otherSites = allSites.filter(site => site.url.replace(/\/$/, '') !== currentUrl.replace(/\/$/, '') ); // Pick a random site return otherSites[Math.floor(Math.random() * otherSites.length)]; }; // Prepare random site const randomSite = getRandomSite(); // Handle ringlet URL and text according to requirements let ringletLinkUrl; let ringletDisplayText; if (!ringlet.name) { // Case 1: No ringlet specified ringletLinkUrl = config.baseUrl; ringletDisplayText = 'webring.fun'; } else if (ringlet.name && !ringlet.url) { // Case 2: Ringlet specified but no URL ringletLinkUrl = `${config.baseUrl}/?ringlet=${config.ringlet}`; ringletDisplayText = `${ringlet.name} webring`; } else { // Case 3: Ringlet specified with URL ringletLinkUrl = ringlet.url; ringletDisplayText = `${ringlet.name} webring`; } // Create tooltip HTML for each site const prevSiteTooltip = ` `; const nextSiteTooltip = ` `; const randomSiteTooltip = ` `; // Create the widget description content const widgetDescription = config.widget_content ? config.widget_content : `This site is a member of ${ringlet.name ? 'the ' : ''}${ringletDisplayText}!`; // Create the widget title const widgetTitle = ringlet.name ? `${ringlet.name} Webring` : `Webring.fun`; // Create content HTML based on extra_details setting let contentHTML; if (config.extra_details) { contentHTML = ` `; } else { contentHTML = ` `; } // Set container content container.innerHTML = contentHTML; // Add styles with dynamic colors addStaticWidgetStyles(siteColor, textColor); // Find the script tag that loaded this widget const scriptTag = document.querySelector('script[id^="webring-"]'); if (scriptTag) { // Insert the widget after the script tag scriptTag.parentNode.insertBefore(container, scriptTag.nextSibling); } else { // Fallback: append to body if script tag not found document.body.appendChild(container); } // Always make the widget visible initially container.classList.add('webring-widget-visible'); } /** * Adds the necessary CSS for the static widget */ function addStaticWidgetStyles(backgroundColor, textColor) { // Check if styles are already added if (document.getElementById('webring-styles')) { return; } // Calculate accent colors const hoverBackground = textColor === "black" ? "rgba(0, 0, 0, 0.05)" : "rgba(255, 255, 255, 0.1)"; const borderColor = textColor === "black" ? "rgba(0, 0, 0, 0.1)" : "rgba(255, 255, 255, 0.2)"; // Create style element const styleEl = document.createElement('style'); styleEl.id = 'webring-styles'; // Define CSS styleEl.textContent = ` .webring-widget-static { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.5; color: ${textColor}; border: 1px solid ${borderColor}; border-radius: 8px; background-color: ${backgroundColor}; margin: 20px 0; overflow: visible; } .webring-widget-full-width { width: 100%; } .webring-widget-static a { color: ${textColor}; text-decoration: none; position: relative; } .webring-widget-static-content { padding: 16px; } .webring-widget-static-header { margin-bottom: 12px; text-align: center; } .webring-widget-static-title { margin: 0; font-size: 16px; font-weight: 600; } .webring-widget-static-title a:hover { text-decoration: underline; } .webring-widget-static-sites { display: flex; flex-wrap: wrap; gap: 16px; } .webring-widget-static-site { flex: 1; min-width: 200px; padding: 12px; border-radius: 6px; background-color: ${hoverBackground}; transition: background-color 0.2s; text-decoration: none; cursor: pointer; display: block; } .webring-widget-static-site:hover { background-color: var(--hover-color); } .webring-widget-icon { vertical-align: -0.125em; margin-right: 4px; display: inline-block; } .webring-widget-static-label { display: flex; align-items: center; font-size: 12px; margin-bottom: 4px; } .webring-widget-static-link-content { display: block; } .webring-widget-static-prev .webring-widget-static-link-content { text-align: left; } .webring-widget-static-random .webring-widget-static-link-content { text-align: center; } .webring-widget-static-next .webring-widget-static-link-content { text-align: right; } .webring-widget-static-random .webring-widget-static-label, .webring-widget-static-next .webring-widget-static-label { justify-content: center; } .webring-widget-static-next .webring-widget-static-label { justify-content: flex-end; } .webring-widget-static-link-content strong { display: block; margin-bottom: 2px; } .webring-widget-static-description { display: block; font-size: 12px; } .webring-widget-static-simple-nav { display: flex; justify-content: center; gap: 12px; } .webring-widget-static-simple-button { padding: 6px 12px; border-radius: 4px; background-color: ${hoverBackground}; transition: background-color 0.2s; position: relative; display: flex; align-items: center; } .webring-widget-static-simple-button:hover { background-color: var(--hover-color); } /* Tooltip styles */ .webring-widget-tooltip { position: absolute; top: -80px; left: 50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.9); color: white; border: none; border-radius: 4px; padding: 8px; width: 160px; font-size: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); opacity: 0; visibility: hidden; transition: opacity 0.2s, visibility 0.2s; pointer-events: none; z-index: 10000; text-align: center; } .webring-widget-tooltip:after { content: ''; position: absolute; bottom: -6px; left: 50%; transform: translateX(-50%); width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 6px solid rgba(0, 0, 0, 0.9); } .webring-widget-tooltip-title { font-weight: bold; margin-bottom: 4px; color: white; text-align: center; } .webring-widget-tooltip-description { font-size: 11px; line-height: 1.3; max-height: 50px; overflow: hidden; text-overflow: ellipsis; color: rgba(255, 255, 255, 0.9); text-align: center; } .webring-widget-static-site:hover .webring-widget-tooltip, .webring-widget-static-simple-button:hover .webring-widget-tooltip { opacity: 1; visibility: visible; } .webring-sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; } @media (max-width: 600px) { .webring-widget-static-sites { flex-direction: column; } .webring-widget-tooltip { width: 140px; } } `; // Set the style content styleEl.textContent = styleEl.textContent.trim(); // Add styles to the document document.head.appendChild(styleEl); } /** * Adds space to the top of the page to prevent content from being hidden under the widget */ function adjustPageForTopWidget(widgetElement) { // Get the height of the widget let widgetHeight = widgetElement.offsetHeight; // Create a style element if it doesn't exist let spacerStyle = document.getElementById('webring-spacer-style'); if (!spacerStyle) { spacerStyle = document.createElement('style'); spacerStyle.id = 'webring-spacer-style'; document.head.appendChild(spacerStyle); } // Set the body padding to match widget height spacerStyle.textContent = ` body { padding-top: ${widgetHeight}px !important; } `; // Set up a resize observer to adjust the padding if widget size changes if (window.ResizeObserver) { const resizeObserver = new ResizeObserver(entries => { for (const entry of entries) { if (entry.target === widgetElement) { const newHeight = entry.contentRect.height; spacerStyle.textContent = ` body { padding-top: ${newHeight}px !important; } `; } } }); resizeObserver.observe(widgetElement); } // Fallback for browsers without ResizeObserver - listen for window resize else { const updatePadding = () => { const newHeight = widgetElement.offsetHeight; spacerStyle.textContent = ` body { padding-top: ${newHeight}px !important; } `; }; window.addEventListener('resize', updatePadding); } } /** * Sets up the slide-out functionality for widgets * This is responsible for adding the webring-widget-visible class */ function setupSlideOut(container) { // For slide-out functionality if (config.slide_toggle) { // Track whether the user has manually toggled the widget let userToggled = false; // Check if we have a saved state in sessionStorage const savedState = sessionStorage.getItem('webring-bar-widget-state'); // Only set up the initial timeout if this is the first visit (no saved state) if (!savedState) { // First visit - initially widget is shown (already set in createBarWidgetDOM) // Set a timeout to hide it after a few seconds setTimeout(() => { // Only auto-hide initially if user hasn't toggled if (!userToggled) { container.classList.remove('webring-widget-visible'); // Save the initial auto-hidden state sessionStorage.setItem('webring-bar-widget-state', 'hidden'); } }, 3000); } // Find the pull tab element const pullTab = container.querySelector('.webring-widget-pull-tab'); // Make sure pull tab is always visible by positioning it outside the container // when the container is slid out if (pullTab) { pullTab.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); userToggled = true; // Mark that user has manually toggled // Toggle visibility const isVisible = container.classList.toggle('webring-widget-visible'); // Store the state in sessionStorage sessionStorage.setItem('webring-bar-widget-state', isVisible ? 'visible' : 'hidden'); }); } // No automatic behavior on hover or mouseleave - only respond to explicit user actions container.addEventListener('mouseenter', () => { // Don't auto-show on hover }); container.addEventListener('mouseleave', () => { // Don't auto-hide on mouseleave }); } // For auto-hide functionality (if enabled) else if (config.auto_hide) { let timer = null; const hideWidget = () => { container.classList.remove('webring-widget-visible'); }; const showWidget = () => { container.classList.add('webring-widget-visible'); // Reset timer if (timer) clearTimeout(timer); // Set new timer for hiding timer = setTimeout(hideWidget, config.hide_delay || 3000); }; // Initially hide after delay timer = setTimeout(hideWidget, config.hide_delay || 3000); // Show on hover or scroll container.addEventListener('mouseenter', showWidget); window.addEventListener('scroll', showWidget, { passive: true }); } } })();