/** * SocialShareButton - A lightweight, customizable social sharing component * @version 1.0.4 * @license GPL-3.0 */ /** Analytics event schema version. Increment when the payload shape changes. */ const ANALYTICS_SCHEMA_VERSION = "1.0"; class SocialShareButton { constructor(options = {}) { // Resolve container element early to prevent duplicate instances const containerEl = SocialShareButton._resolveContainer(options.container); if (containerEl && containerEl._socialShareButtonInstance) { return containerEl._socialShareButtonInstance; } this.options = { url: options.url || (typeof window !== "undefined" ? window.location.href : ""), title: options.title || (typeof document !== "undefined" ? document.title : ""), description: options.description || "", hashtags: options.hashtags || [], via: options.via || "", platforms: options.platforms || [ "whatsapp", "facebook", "twitter", "linkedin", "telegram", "reddit", "pinterest", "discord", ], theme: options.theme || "dark", buttonText: options.buttonText || "Share", customClass: options.customClass || "", buttonColor: options.buttonColor || "", buttonHoverColor: options.buttonHoverColor || "", onShare: options.onShare || null, onCopy: options.onCopy || null, container: options.container || null, showButton: options.showButton !== false, buttonStyle: options.buttonStyle || "default", modalPosition: options.modalPosition || "center", // Analytics — the library emits events but never collects or sends data itself. // Website owners wire up their own analytics tools via these options. analytics: options.analytics !== false, // set to false to disable all event emission onAnalytics: options.onAnalytics || null, // callback: (payload) => void analyticsPlugins: options.analyticsPlugins || [], // array of { track(payload) } adapters componentId: options.componentId || null, // optional identifier for this instance debug: options.debug || false, // log emitted events to console in development }; this.isModalOpen = false; this.modal = null; this.button = null; this.customColorMouseEnterHandler = null; this.customColorMouseLeaveHandler = null; this.handleKeydown = null; this.listeners = []; // Central registry for all event listeners this.openTimeout = null; // Track setTimeout for openModal animation this.closeTimeout = null; // Track setTimeout for closeModal animation this.feedbackTimeout = null; // Track setTimeout for copy feedback reset this.ownsBodyLock = false; // Track if this instance owns the body overflow lock this.eventsAttached = false; // Guard against multiple attachEvents() calls this.isDestroyed = false; // Track if instance has been destroyed (prevents async callbacks) this._dynamicUrl = !options.url; // true when url was not provided; updateCurrentPage() will overwrite on SPA route changes this._dynamicTitle = !options.title; // true when title was not provided; updateCurrentPage() will overwrite on SPA route changes this._containerEl = containerEl; // Cache resolved element so cleanup still finds it after removal // If a container was specified but could not be resolved (e.g. SSR or missing DOM node), // abort registration and initialization to prevent orphan instances and DOM errors. if (this.options.container && !this._containerEl) { return; } if (containerEl) { containerEl._socialShareButtonInstance = this; } if (typeof window !== "undefined" && SocialShareButton.instances) { SocialShareButton.instances.add(this); } if (this.options.container) { this.init(); } } init() { if (this.options.showButton) { this.createButton(); } this.createModal(); this.attachEvents(); this.applyCustomColors(); } createButton() { const button = document.createElement("button"); button.className = `social-share-btn ${this.options.buttonStyle} ${this.options.customClass}`; button.setAttribute("aria-label", "Share"); button.innerHTML = ` ${this.options.buttonText} `; this.button = button; if (this._containerEl) { this._containerEl.appendChild(button); } } createModal() { const modal = document.createElement("div"); modal.className = `social-share-modal-overlay ${this.options.theme}`; modal.style.display = "none"; modal.innerHTML = `

Share

${this.getPlatformsHTML()}
`; const urlInputContainer = modal.querySelector(".social-share-link-input"); const urlInput = document.createElement("input"); urlInput.type = "text"; urlInput.value = this.options.url; urlInput.readOnly = true; urlInput.setAttribute("aria-label", "URL to share"); urlInputContainer.appendChild(urlInput); this.modal = modal; document.body.appendChild(modal); } getPlatformsHTML() { const platforms = { whatsapp: { name: "WhatsApp", color: "#25D366", icon: '', }, facebook: { name: "Facebook", color: "#1877F2", icon: '', }, twitter: { name: "X", color: "#000000", icon: '', }, linkedin: { name: "LinkedIn", color: "#0A66C2", icon: '', }, telegram: { name: "Telegram", color: "#0088cc", icon: '', }, reddit: { name: "Reddit", color: "#FF4500", icon: '', }, email: { name: "Email", color: "#7f7f7f", icon: '', }, pinterest: { name: "Pinterest", color: "#E60023", icon: '', }, discord: { name: "Discord", color: "#5865F2", icon: '', }, }; return this.options.platforms .filter((platform) => platforms[platform]) .map((platform) => { const { name, color, icon } = platforms[platform]; return ` `; }) .join(""); } getShareURL(platform) { const { url, title, description, hashtags, via } = this.options; const encodedUrl = encodeURIComponent(url); const encodedTitle = encodeURIComponent(title); // const encodedDesc = encodeURIComponent(description); const hashtagString = hashtags.length ? "#" + hashtags.join(" #") : ""; // Build platform-specific messages with customizable parameters let whatsappMessage, facebookMessage, twitterMessage, telegramMessage, redditTitle, emailBody, pinterestText; // WhatsApp: Casual with emoji whatsappMessage = `\u{1F680} ${title}${description ? "\n\n" + description : ""}${hashtagString ? "\n\n" + hashtagString : ""}\n\nLive on the site \u{1F440}\nClean UI, smooth flow \u{2014} worth peeking\n\u{1F447}`; // Facebook: Title + Description facebookMessage = `${title}${description ? "\n\n" + description : ""}${hashtagString ? "\n\n" + hashtagString : ""}`; // Twitter: Title + Description + Hashtags + Via twitterMessage = `${title}${description ? "\n\n" + description : ""}${hashtagString ? "\n" + hashtagString : ""}`; // Telegram: Casual with emoji telegramMessage = `\u{1F517} ${title}${description ? "\n\n" + description : ""}${hashtagString ? "\n\n" + hashtagString : ""}\n\nLive + working\nClean stuff, take a look \u{1F447}`; // Reddit: Title + Description redditTitle = `${title}${description ? " - " + description : ""}`; // Email: Friendly greeting emailBody = `Hey \u{1F44B}\n\nSharing a clean project I came across:\n${title}${description ? "\n\n" + description : ""}\n\nLive, simple, and usable \u{2014} take a look \u{1F447}`; // Pinterest: Title + Description pinterestText = `${title || ""}${description ? " - " + description : ""}`; const encodedWhatsapp = encodeURIComponent(whatsappMessage); const encodedFacebook = encodeURIComponent(facebookMessage); const encodedTwitter = encodeURIComponent(twitterMessage); const encodedTelegram = encodeURIComponent(telegramMessage); const encodedReddit = encodeURIComponent(redditTitle); const encodedEmail = encodeURIComponent(emailBody); const encodedPinterest = encodeURIComponent(pinterestText); const urls = { whatsapp: `https://wa.me/?text=${encodedWhatsapp}%20${encodedUrl}`, facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}"e=${encodedFacebook}`, twitter: `https://twitter.com/intent/tweet?text=${encodedTwitter}&url=${encodedUrl}${via ? "&via=" + encodeURIComponent(via) : ""}`, linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`, telegram: `https://t.me/share/url?url=${encodedUrl}&text=${encodedTelegram}`, reddit: `https://reddit.com/submit?url=${encodedUrl}&title=${encodedReddit}`, email: `mailto:?subject=${encodedTitle}&body=${encodedEmail}%20${encodedUrl}`, pinterest: `https://pinterest.com/pin/create/button/?url=${encodedUrl}&description=${encodedPinterest}`, discord: "https://discord.com/channels/@me", }; return urls[platform] || ""; } addEventListener(element, type, handler, options = false) { if (!element) return; element.addEventListener(type, handler, options); this.listeners.push({ element, type, handler, options }); } //Remove all tracked event listeners (used in destroy to prevent memory leaks) removeAllListeners() { this.listeners.forEach(({ element, type, handler, options }) => { if (element) { element.removeEventListener(type, handler, options); } }); this.listeners = []; } attachEvents() { // Re-entrancy guard: prevent double-registration if called multiple times if (this.eventsAttached) return; // Button click to open modal if (this.button) { const openModalHandler = () => this.openModal(); this.addEventListener(this.button, "click", openModalHandler); } // Modal overlay click to close const modalClickHandler = (e) => { if (e.target === this.modal) { this.closeModal(); } }; this.addEventListener(this.modal, "click", modalClickHandler); // Close button const closeBtn = this.modal.querySelector(".social-share-modal-close"); const closeBtnHandler = () => this.closeModal(); this.addEventListener(closeBtn, "click", closeBtnHandler); // Platform buttons const platformBtns = this.modal.querySelectorAll(".social-share-platform-btn"); platformBtns.forEach((btn) => { const platformHandler = () => { const platform = btn.dataset.platform; this.share(platform); }; this.addEventListener(btn, "click", platformHandler); }); // Copy button const copyBtn = this.modal.querySelector(".social-share-copy-btn"); const copyBtnHandler = () => this.copyLink(); this.addEventListener(copyBtn, "click", copyBtnHandler); // Input click to select const input = this.modal.querySelector(".social-share-link-input input"); const inputSelectHandler = (e) => e.target.select(); this.addEventListener(input, "click", inputSelectHandler); // ESC key to close this.handleKeydown = (e) => { if (e.key === "Escape" && this.isModalOpen) { this.closeModal(); } }; if (typeof document !== "undefined") { document.addEventListener("keydown", this.handleKeydown); } this.eventsAttached = true; // Mark as attached } openModal() { // Safety check: prevent errors if modal was destroyed if (!this.modal) return; this.isModalOpen = true; this.modal.style.display = "flex"; this._emit("social_share_popup_open", "popup_open"); // Shared body overflow management: only increment counter if this instance doesn't already own the lock if (typeof document !== "undefined" && document.body) { if (!this.ownsBodyLock) { // Only increment if this instance doesn't already own a lock if (SocialShareButton.openModalCount === 0) { // Save original overflow before first modal opens SocialShareButton.originalBodyOverflow = document.body.style.overflow; } SocialShareButton.openModalCount++; this.ownsBodyLock = true; // Mark that this instance owns a lock } document.body.style.overflow = "hidden"; } // Clear any pending animations (both open and close to prevent race conditions) if (this.openTimeout) { clearTimeout(this.openTimeout); this.openTimeout = null; } if (this.closeTimeout) { clearTimeout(this.closeTimeout); this.closeTimeout = null; } // Animate in this.openTimeout = setTimeout(() => { if (this.modal) { // Safety check in case destroy() was called this.modal.classList.add("active"); } this.openTimeout = null; }, 10); } closeModal() { if (!this.modal) return; // Safety check this.modal.classList.remove("active"); this._emit("social_share_popup_close", "popup_close"); // Clear any pending animations (both open and close to prevent race conditions) if (this.openTimeout) { clearTimeout(this.openTimeout); this.openTimeout = null; } if (this.closeTimeout) { clearTimeout(this.closeTimeout); this.closeTimeout = null; } this.closeTimeout = setTimeout(() => { if (this.modal) { // Safety check in case destroy() was called this.isModalOpen = false; this.modal.style.display = "none"; // Shared body overflow management: only decrement if this instance owns the lock if (this.ownsBodyLock && typeof document !== "undefined" && document.body) { // Decrement counter (guard against negative) if (SocialShareButton.openModalCount > 0) { SocialShareButton.openModalCount--; } this.ownsBodyLock = false; // Release the lock // Restore original overflow only when all modals are closed if (SocialShareButton.openModalCount === 0) { document.body.style.overflow = SocialShareButton.originalBodyOverflow || ""; SocialShareButton.originalBodyOverflow = null; } } } this.closeTimeout = null; }, 200); } share(platform) { const shareUrl = this.getShareURL(platform); if (shareUrl) { this._emit("social_share_click", "share", { platform }); // Platform-specific sharing trigger logic: // Discord does not provide a native web-share intent; therefore, the most reliable // fallback is to copy the share URL to the clipboard and then navigate the user // towards Discord's direct messaging area. if (platform === "discord") { this.copyLink(); window.open(shareUrl, "_blank", "noopener,noreferrer"); } else if (platform === "email") { window.location.href = shareUrl; } else { // Default behavior: open platform's share intent in a specialized popup window window.open(shareUrl, "_blank", "noopener,noreferrer,width=600,height=600"); } this._emit("social_share_success", "share", { platform }); if (this.options.onShare) { this.options.onShare(platform, this.options.url); } } else { this._emit("social_share_error", "error", { platform, errorMessage: `No share URL configured for platform: ${platform}`, }); } } copyLink() { const input = this.modal.querySelector(".social-share-link-input input"); const copyBtn = this.modal.querySelector(".social-share-copy-btn"); // Check if clipboard API is available if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard .writeText(this.options.url) .then(() => { // Guard against async callback after destroy if (this.isDestroyed) return; copyBtn.textContent = "Copied!"; copyBtn.classList.add("copied"); this._emit("social_share_copy", "copy"); if (this.options.onCopy) { this.options.onCopy(this.options.url); } // Clear any existing feedback timeout if (this.feedbackTimeout) { clearTimeout(this.feedbackTimeout); } // Track feedback timeout to prevent callback after destroy this.feedbackTimeout = setTimeout(() => { if (this.isDestroyed || !copyBtn) return; // Safety check copyBtn.textContent = "Copy"; copyBtn.classList.remove("copied"); this.feedbackTimeout = null; }, 2000); }) .catch(() => { // Fallback to manual selection this.fallbackCopy(input, copyBtn); }); } else { // Fallback for browsers without clipboard API this.fallbackCopy(input, copyBtn); } } fallbackCopy(input, copyBtn) { // Guard against execution after destroy if (this.isDestroyed) return; try { input.select(); input.setSelectionRange(0, 99999); // For mobile devices document.execCommand("copy"); copyBtn.textContent = "Copied!"; copyBtn.classList.add("copied"); this._emit("social_share_copy", "copy"); if (this.options.onCopy) { this.options.onCopy(this.options.url); } // Clear any existing feedback timeout if (this.feedbackTimeout) { clearTimeout(this.feedbackTimeout); } // Track feedback timeout to prevent callback after destroy this.feedbackTimeout = setTimeout(() => { if (this.isDestroyed || !copyBtn) return; // Safety check copyBtn.textContent = "Copy"; copyBtn.classList.remove("copied"); this.feedbackTimeout = null; }, 2000); } catch (_err) { copyBtn.textContent = "Failed"; // Clear any existing feedback timeout if (this.feedbackTimeout) { clearTimeout(this.feedbackTimeout); } // Track feedback timeout to prevent callback after destroy this.feedbackTimeout = setTimeout(() => { if (this.isDestroyed || !copyBtn) return; // Safety check copyBtn.textContent = "Copy"; this.feedbackTimeout = null; }, 2000); } } destroy() { if (this.handleKeydown) { if (typeof document !== "undefined") { document.removeEventListener("keydown", this.handleKeydown); } this.handleKeydown = null; } // Mark as destroyed to prevent async callbacks this.isDestroyed = true; // Remove all tracked event listeners (prevents memory leaks) this.removeAllListeners(); // Clear any pending animation timeouts to prevent accessing null references if (this.openTimeout) { clearTimeout(this.openTimeout); this.openTimeout = null; } if (this.closeTimeout) { clearTimeout(this.closeTimeout); this.closeTimeout = null; } if (this.feedbackTimeout) { clearTimeout(this.feedbackTimeout); this.feedbackTimeout = null; } // Remove custom color handlers if (this.button && this.customColorMouseEnterHandler) { this.button.removeEventListener("mouseenter", this.customColorMouseEnterHandler); this.customColorMouseEnterHandler = null; } if (this.button && this.customColorMouseLeaveHandler) { this.button.removeEventListener("mouseleave", this.customColorMouseLeaveHandler); this.customColorMouseLeaveHandler = null; } // Remove DOM elements if (this.button && this.button.parentNode) { this.button.parentNode.removeChild(this.button); } if (this.modal && this.modal.parentNode) { this.modal.parentNode.removeChild(this.modal); } // Shared body overflow management: only decrement if this instance owns the lock if (this.ownsBodyLock && typeof document !== "undefined" && document.body) { // Decrement counter (guard against negative) if (SocialShareButton.openModalCount > 0) { SocialShareButton.openModalCount--; } this.ownsBodyLock = false; // Release the lock // Restore original overflow only when all modals are closed if (SocialShareButton.openModalCount === 0) { document.body.style.overflow = SocialShareButton.originalBodyOverflow || ""; SocialShareButton.originalBodyOverflow = null; } } // Remove from active instances registry if (typeof window !== "undefined" && SocialShareButton.instances) { SocialShareButton.instances.delete(this); } const containerEl = this._getContainer(); if (containerEl && containerEl._socialShareButtonInstance === this) { delete containerEl._socialShareButtonInstance; } // Clear references (makes destroy idempotent) this.button = null; this.modal = null; this.isModalOpen = false; this.eventsAttached = false; // Reset re-entrancy guard } updateOptions(options, isInternalRefresh = false) { // External updates recompute dynamic flags, while internal SPA refreshes // preserve caller-supplied values to maintain the auto-update contract. if (!isInternalRefresh) { if (options.url !== undefined) { this._dynamicUrl = !options.url; } if (options.title !== undefined) { this._dynamicTitle = !options.title; } } this.options = { ...this.options, ...options }; // Update URL in modal if it exists if (this.modal) { const input = this.modal.querySelector(".social-share-link-input input"); if (input) { input.value = this.options.url; } } // Reapply custom colors if color option changed if ("buttonColor" in options || "buttonHoverColor" in options) { this.applyCustomColors(); } } applyCustomColors() { if (!this.button) return; // Remove legacy global style tag to prevent cross-instance color bleed. const styleTag = document.getElementById("social-share-custom-colors"); if (styleTag && styleTag.parentNode) { styleTag.parentNode.removeChild(styleTag); } if (this.customColorMouseEnterHandler) { this.button.removeEventListener("mouseenter", this.customColorMouseEnterHandler); this.customColorMouseEnterHandler = null; } if (this.customColorMouseLeaveHandler) { this.button.removeEventListener("mouseleave", this.customColorMouseLeaveHandler); this.customColorMouseLeaveHandler = null; } this.button.style.removeProperty("background-color"); this.button.style.removeProperty("background-image"); this.button.style.removeProperty("border-color"); const baseColor = this.options.buttonColor || ""; const hoverColor = this.options.buttonHoverColor || baseColor; if (!baseColor && !hoverColor) return; if (baseColor) { this.button.style.backgroundImage = "none"; this.button.style.backgroundColor = baseColor; this.button.style.borderColor = baseColor; } this.customColorMouseEnterHandler = () => { if (hoverColor) { this.button.style.backgroundImage = "none"; this.button.style.backgroundColor = hoverColor; this.button.style.borderColor = hoverColor; } }; this.customColorMouseLeaveHandler = () => { if (baseColor) { this.button.style.backgroundImage = "none"; this.button.style.backgroundColor = baseColor; this.button.style.borderColor = baseColor; } else { this.button.style.removeProperty("background-color"); this.button.style.removeProperty("background-image"); this.button.style.removeProperty("border-color"); } }; // Note: Custom color handlers are managed separately (not in listeners) // because they need to be removed/reapplied when colors change this.button.addEventListener("mouseenter", this.customColorMouseEnterHandler); this.button.addEventListener("mouseleave", this.customColorMouseLeaveHandler); } // --------------------------------------------------------------------------- // Analytics event system // // The library is privacy-by-design: it never collects, stores, or transmits // user data. _emit() only dispatches interaction events locally so that the // host website can forward them to whichever analytics tool they choose. // // Three delivery paths run in parallel for maximum flexibility: // 1. DOM CustomEvent — works with CDN drops, vanilla JS, and any framework. // Multiple independent listeners can subscribe with // document.addEventListener('social-share', handler). // 2. onAnalytics hook — single direct callback, useful for inline setups. // 3. analyticsPlugins — adapter registry; each adapter's track() method is // called in turn, allowing GA4 + Mixpanel + custom // systems to all receive the same event simultaneously. // --------------------------------------------------------------------------- // Resolves a raw container value (string or Element) to a DOM Element, or null if absent/SSR. static _resolveContainer(raw) { if (!raw) return null; if (typeof document === "undefined") return null; return typeof raw === "string" ? document.querySelector(raw) : raw; } // Returns the cached host container element, or null. _getContainer() { return this._containerEl || null; } /** * Logs analytics warnings only when debug mode is enabled. * @param {string} message - Description of the failed analytics path. * @param {Error} err - The caught error instance. */ _debugWarn(message, err) { // _debugWarn: emit analytics warnings only in debug mode for visibility. if (!this.options.debug) return; // eslint-disable-next-line no-console console.warn("[SocialShareButton Analytics]", message, err); } /** * Emits an analytics event through all configured delivery paths. * * Standard payload schema * ───────────────────────────────────────────────────────────────────────── * { * eventName : string — e.g. 'social_share_click' * interactionType: string — 'share' | 'copy' | 'popup_open' | * 'popup_close' | 'error' * platform : string|null — 'twitter', 'facebook', etc. * url : string — URL being shared * title : string — page title * timestamp : number — Unix ms (Date.now()) * componentId : string|null — value of the componentId option * errorMessage : string — only present on social_share_error events * } * * @param {string} eventName - snake_case event identifier * @param {string} interactionType - broad interaction category * @param {Object} [extra] - optional extra fields (platform, errorMessage) */ _emit(eventName, interactionType, extra = {}) { if (this.options.analytics === false) return; const payload = { version: ANALYTICS_SCHEMA_VERSION, source: "social-share-button", eventName, interactionType, platform: extra.platform || null, url: this.options.url, title: this.options.title, timestamp: Date.now(), componentId: this.options.componentId, }; if (extra.errorMessage) { payload.errorMessage = extra.errorMessage; } // Optional console output for development / debugging if (this.options.debug) { // eslint-disable-next-line no-console console.log("[SocialShareButton Analytics]", payload); } // Path 1 — DOM CustomEvent (framework-agnostic, CDN-friendly) // Bubbles from the container element so delegated listeners work naturally. if (typeof window !== "undefined" && typeof CustomEvent !== "undefined") { try { const domEvent = new CustomEvent("social-share", { bubbles: true, cancelable: false, composed: true, // crosses shadow-DOM boundaries; safe to set in all envs detail: payload, }); const el = this._getContainer(); (el || document).dispatchEvent(domEvent); } catch (err) { this._debugWarn("DOM event dispatch failed", err); } } // Path 2 — onAnalytics callback (direct, single-consumer hook) if (typeof this.options.onAnalytics === "function") { try { this.options.onAnalytics(payload); } catch (err) { this._debugWarn("onAnalytics callback failed", err); } } // Path 3 — plugin / adapter registry (supports multiple simultaneous consumers) if (Array.isArray(this.options.analyticsPlugins)) { for (const plugin of this.options.analyticsPlugins) { if (plugin && typeof plugin.track === "function") { try { plugin.track(payload); } catch (err) { this._debugWarn("plugin.track() failed", err); } } } } } static updateCurrentPage() { if (typeof window === "undefined" || !SocialShareButton.instances) return; SocialShareButton.instances.forEach((instance) => { // Only refresh fields that were auto-defaulted from the page; // caller-supplied url/title values must remain unchanged. const updates = {}; if (instance._dynamicUrl) { updates.url = window.location.href; } if (instance._dynamicTitle) { updates.title = document.title; } if (Object.keys(updates).length > 0) { instance.updateOptions(updates, true); } }); } } // Static properties for shared body overflow management across all instances SocialShareButton.openModalCount = 0; SocialShareButton.originalBodyOverflow = null; SocialShareButton.instances = new Set(); (function () { // Helper for dynamic route changes in SPAs function handleRouteChange() { setTimeout(() => { SocialShareButton.updateCurrentPage(); }, 50); } // Patch pushState and replaceState to track SPA routing dynamically let isHistoryPatched = false; function patchHistory() { if (typeof window === "undefined" || !window.history || isHistoryPatched) return; isHistoryPatched = true; const wrap = (type) => { const orig = window.history[type]; return function (...args) { const result = orig.apply(this, args); try { const event = new CustomEvent("pushstate-or-replacestate", { detail: { type } }); window.dispatchEvent(event); } catch (_e) { // Ignore error } return result; }; }; window.history.pushState = wrap("pushState"); window.history.replaceState = wrap("replaceState"); } // Auto-initialize elements with `data-social-share` attribute function autoInitElement(element) { if (element._socialShareButtonInstance) return; const options = { container: element }; const attrs = { url: "data-url", title: "data-title", description: "data-description", via: "data-via", theme: "data-theme", buttonText: "data-button-text", customClass: "data-custom-class", buttonColor: "data-button-color", buttonHoverColor: "data-button-hover-color", buttonStyle: "data-button-style", modalPosition: "data-modal-position", }; for (const [key, attr] of Object.entries(attrs)) { const val = element.getAttribute(attr); if (val !== null) { options[key] = val; } } if (element.hasAttribute("data-hashtags")) { options.hashtags = element .getAttribute("data-hashtags") .split(",") .map((s) => s.trim()) .filter(Boolean); } if (element.hasAttribute("data-platforms")) { options.platforms = element .getAttribute("data-platforms") .split(",") .map((s) => s.trim()) .filter(Boolean); } if (element.hasAttribute("data-show-button")) { options.showButton = element.getAttribute("data-show-button") !== "false"; } new SocialShareButton(options); } function autoInit(root = document) { if (typeof document === "undefined") return; // Fast short-circuit: skip nodes appended by the library itself to avoid redundant scans if ( root !== document && root.classList && (root.classList.contains("social-share-btn") || root.classList.contains("social-share-modal-overlay")) ) { return; } if (root !== document && root.hasAttribute && root.hasAttribute("data-social-share")) { autoInitElement(root); } const elements = root.querySelectorAll ? root.querySelectorAll("[data-social-share]") : []; elements.forEach((element) => autoInitElement(element)); } // Setup MutationObserver to watch for added/removed sharing components let globalObserver = null; let initTimeout = null; function setupMutationObserver() { if (typeof document === "undefined" || globalObserver) return; globalObserver = new MutationObserver((mutations) => { let needsInit = false; mutations.forEach((mutation) => { // Auto-cleanup on removal // Fast-path: Skip costly removal traversal if we have no active tracked instances if ( mutation.removedNodes.length > 0 && SocialShareButton.instances && SocialShareButton.instances.size > 0 ) { mutation.removedNodes.forEach((node) => { if (node.nodeType !== 1) return; SocialShareButton.instances.forEach((instance) => { const containerEl = instance._getContainer(); if (containerEl && (node === containerEl || node.contains(containerEl))) { instance.destroy(); } }); }); } // Track if there are any element additions to trigger a batched scan if (!needsInit && mutation.addedNodes.length > 0) { for (let i = 0; i < mutation.addedNodes.length; i++) { if (mutation.addedNodes[i].nodeType === 1) { needsInit = true; break; } } } }); // Batch auto-init to avoid costly scans on every single added node. // If a timeout is already pending, we let it run (throttle vs debounce) // so continuous DOM mutations don't indefinitely delay initialization. if (needsInit && !initTimeout) { initTimeout = setTimeout(() => { initTimeout = null; autoInit(document); // Single global scan is highly optimized natively }, 10); } }); globalObserver.observe(document.body, { childList: true, subtree: true }); } // Initialize on browser load if (typeof window !== "undefined" && typeof document !== "undefined") { patchHistory(); window.addEventListener("popstate", handleRouteChange); window.addEventListener("hashchange", handleRouteChange); window.addEventListener("pushstate-or-replacestate", handleRouteChange); if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => { autoInit(); setupMutationObserver(); }); } else { autoInit(); setupMutationObserver(); } } })(); // Export for different module systems if (typeof module !== "undefined" && module.exports) { module.exports = SocialShareButton; } if (typeof window !== "undefined") { window.SocialShareButton = SocialShareButton; }