From d0c006ea7394c119a5c1f96da5d75319045c245f Mon Sep 17 00:00:00 2001 From: Muneer Ali Date: Sun, 15 Mar 2026 12:54:41 +0530 Subject: [PATCH 01/16] feat: intelligent content auto-detection for SocialShareButton using Readability.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements GitHub Issue #26 — auto-generate share text based on page content. ## What changed ### src/utils/extractContent.js (new) Standalone content-detection utility (zero external dependencies). Detection priority: Title: og:title → twitter:title → article/main h1 → .post-title / .entry-title → document.title Description: og:description → twitter:description → meta[name=description] → body text excerpt (150-200 chars) Results cached per-call for 30 s; cache can be busted via clearContentCache() for SPA navigation. ### src/social-share-button.js - Inlined _ContentDetector IIFE (mirrors extractContent logic, keeps CDN build zero-dep) - Constructor auto-detects title/description when props are omitted - Manual props always override detection (fully backward-compatible) - New autoDetect: false option to opt out entirely - Static SocialShareButton.clearContentCache() for SPA route-change support - Fixed duplicate window.SocialShareButton assignment at end of file - Added ES module export default for Node/bundler compatibility - Added comments to previously-empty catch blocks (fixes lint no-empty rule) ### src/social-share-button-react.jsx - Added autoDetect prop (default: true) - Calls SocialShareButton.clearContentCache() on prop/route change when autoDetect is enabled ### tests/ (new directory) - tests/extractContent.test.js — 16 unit tests covering title/description detection, content root selection, noise stripping, caching, and error resilience - tests/socialShareButton.autodetect.test.js — 12 integration tests covering auto-detection, manual override, autoDetect:false, fallback, updateOptions, and clearContentCache static method All 28 tests pass using Node built-in test runner (node:test) + jsdom. ### README.md Added 'Automatic Content Detection' section documenting detection priority, zero-config usage, manual override, autoDetect:false, and SPA cache clearing. Updated options table with autoDetect entry. ### package.json - Added jsdom devDependency for DOM simulation in tests - Added test script: node --test tests/**/*.test.js - Added src/utils/extractContent.js to files array --- README.md | 265 +++++++---- package-lock.json | 523 +++++++++++++++++++++ package.json | 5 +- src/social-share-button-react.jsx | 66 +-- src/social-share-button.js | 516 +++++++++++++------- src/utils/extractContent.js | 275 +++++++++++ tests/extractContent.test.js | 287 +++++++++++ tests/socialShareButton.autodetect.test.js | 191 ++++++++ 8 files changed, 1836 insertions(+), 292 deletions(-) create mode 100644 src/utils/extractContent.js create mode 100644 tests/extractContent.test.js create mode 100644 tests/socialShareButton.autodetect.test.js diff --git a/README.md b/README.md index bf8c605..78441bd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@
- > ⚠️ **IMPORTANT** > > All project discussions happens on **[Discord](https://discord.com/channels/1022871757289422898/1479012884209078365)**. @@ -75,6 +74,7 @@ Lightweight social sharing component for web applications. Zero dependencies, fr - 🎯 Zero dependencies - pure vanilla JavaScript - ⚛️ Framework support: React, Next.js, Vue, Angular, or plain HTML - 🔄 Auto-detects current URL and page title +- 🧠 Intelligent content detection — automatically extracts title & description from page metadata and semantic HTML - 📱 Fully responsive and mobile-ready - 🎨 Customizable themes (dark/light) - ⚡ Lightweight (< 10KB gzipped) @@ -86,7 +86,10 @@ Lightweight social sharing component for web applications. Zero dependencies, fr ### Via CDN (Recommended) ```html - + ``` @@ -101,11 +104,11 @@ Lightweight social sharing component for web applications. Zero dependencies, fr No matter which framework you use, integration always follows the same 3 steps: -| Step | What to do | Where | -|------|-----------|-------| -| **1️⃣ Load Library** | Add CSS + JS (CDN links) | Global layout file — `index.html` / `layout.tsx` / `_document.tsx` | -| **2️⃣ Add Container** | Place `
` | The UI component where you want the button to appear | -| **3️⃣ Initialize** | Call `new SocialShareButton({ container: "#share-button" })` | Inside that component, after the DOM is ready (e.g. `useEffect`, `mounted`, `ngAfterViewInit`) | +| Step | What to do | Where | +| -------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | +| **1️⃣ Load Library** | Add CSS + JS (CDN links) | Global layout file — `index.html` / `layout.tsx` / `_document.tsx` | +| **2️⃣ Add Container** | Place `
` | The UI component where you want the button to appear | +| **3️⃣ Initialize** | Call `new SocialShareButton({ container: "#share-button" })` | Inside that component, after the DOM is ready (e.g. `useEffect`, `mounted`, `ngAfterViewInit`) | > 💡 Pick your framework below for the full copy-paste snippet: @@ -132,8 +135,8 @@ No matter which framework you use, integration always follows the same 3 steps: Open an **existing** component that renders on every page — typically `src/components/Header.jsx`, `src/layouts/MainLayout.jsx`, or your root `App.jsx`. Add the snippet below to that component so the share button is consistently available across your app. ```jsx -import { useEffect, useRef } from "react"; -import { useLocation } from "react-router-dom"; // omit if not using React Router +import { useEffect, useRef } from 'react'; +import { useLocation } from 'react-router-dom'; // omit if not using React Router // ⬇️ Replace 'Header' with the name of the component where you want the // share button to appear — e.g. Navbar, MainLayout, App, etc. @@ -146,7 +149,7 @@ function Header() { if (initRef.current || !window.SocialShareButton) return; shareButtonRef.current = new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); initRef.current = true; @@ -184,13 +187,9 @@ function Header() { ### Step 1: Add CDN to `app/layout.tsx` ```tsx -import Script from "next/script"; +import Script from 'next/script'; -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( @@ -216,10 +215,10 @@ export default function RootLayout({ Because `SocialShareButton` manipulates the DOM, it must run inside a **Client Component** (note the `"use client"` directive at the top). Add the snippet below to an existing component such as `app/components/Header.tsx` or `app/components/Navbar.tsx` — any component already included in your layout. ```tsx -"use client"; +'use client'; -import { useEffect, useRef } from "react"; -import { usePathname } from "next/navigation"; +import { useEffect, useRef } from 'react'; +import { usePathname } from 'next/navigation'; // ⬇️ Replace 'Header' with the name of the component where you want the // share button to appear — e.g. Navbar, MainLayout, App, etc. @@ -231,11 +230,10 @@ export default function Header() { useEffect(() => { const initButton = () => { - if (initRef.current || !window.SocialShareButton || !containerRef.current) - return; + if (initRef.current || !window.SocialShareButton || !containerRef.current) return; shareButtonRef.current = new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); initRef.current = true; }; @@ -299,7 +297,7 @@ declare global { ### Step 1: Add CDN to `pages/_document.tsx` ```tsx -import { Html, Head, Main, NextScript } from "next/document"; +import { Html, Head, Main, NextScript } from 'next/document'; export default function Document() { return ( @@ -325,8 +323,8 @@ export default function Document() { Open an existing component that is rendered on every page — typically `components/Header.tsx`, `components/Navbar.tsx`, or `components/Layout.tsx`. Since `_document.tsx` loads the script globally, the button is ready to initialize in any of these components. ```tsx -import { useEffect, useRef } from "react"; -import { useRouter } from "next/router"; +import { useEffect, useRef } from 'react'; +import { useRouter } from 'next/router'; // ⬇️ Replace 'Header' with the name of the component where you want the // share button to appear — e.g. Navbar, MainLayout, App, etc. @@ -338,11 +336,10 @@ export default function Header() { useEffect(() => { const initButton = () => { - if (initRef.current || !window.SocialShareButton || !containerRef.current) - return; + if (initRef.current || !window.SocialShareButton || !containerRef.current) return; shareButtonRef.current = new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); initRef.current = true; }; @@ -426,7 +423,7 @@ Open your root or layout component (e.g., `App.vue`, `app.component.html`, or `A // Add
to your component's template/HTML first, // then initialize once the DOM is ready (e.g., in mounted(), ngAfterViewInit(), or useEffect()): new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); ``` @@ -434,43 +431,123 @@ new window.SocialShareButton({ --- +## Automatic Content Detection + +SocialShareButton can automatically detect the title and description for sharing from your page — no manual configuration needed. + +### How It Works + +When `title` or `description` props are not provided, the component runs a lightweight detection pass over the page: + +| Priority | Signal | Used for | +| -------- | -------------------------------------------------------- | ---------------------- | ---------------- | +| 1 | `` | Title | +| 2 | `` | Title | +| 3 | First `

` in `
` / `
` / `[role="main"]` | Title | +| 4 | Common CMS selectors (`.post-title`, `.entry-title`, …) | Title | +| 5 | `document.title` (strips ` | Site Name` suffixes) | Title (fallback) | +| 1 | `` | Description | +| 2 | `` | Description | +| 3 | `` | Description | +| 4 | Main content body excerpt (150–200 chars) | Description (fallback) | + +No external libraries are loaded. The detector is inlined directly in the script — zero added bytes to `node_modules` and no network requests. + +### Zero-Config Usage + +```html + +
+ +``` + +Or in React: + +```jsx +// No title or description props needed — auto-detected from the page + +``` + +### Manual Props Always Win + +If you supply `title` or `description`, auto-detection is bypassed for those fields. You can mix-and-match: + +```jsx +new SocialShareButton({ + container: '#share-button', + title: 'My Custom Title', // explicit — used as-is + // description omitted — auto-detected from page meta/body +}); +``` + +### Disabling Auto-Detection + +```jsx +new SocialShareButton({ + container: '#share-button', + autoDetect: false, // skip detection entirely + title: 'My Title', + description: 'My description', +}); +``` + +### SPA / Client-Side Navigation + +When the route changes, call `clearContentCache()` before updating options so the next detection picks up fresh page content: + +```jsx +// React Router / Next.js +useEffect(() => { + SocialShareButton.clearContentCache(); + shareButtonRef.current?.updateOptions({ + url: window.location.href, + title: document.title, + }); +}, [pathname]); +``` + +--- + ## Configuration ### Basic Options ```jsx new SocialShareButton({ - container: "#share-button", // Required: CSS selector or DOM element - url: "https://example.com", // Optional: defaults to window.location.href - title: "Custom Title", // Optional: defaults to document.title - buttonText: "Share", // Optional: button label text - buttonStyle: "primary", // default | primary | compact | icon-only - theme: "dark", // dark | light - platforms: ["twitter", "linkedin"], // Optional: defaults to all platforms + container: '#share-button', // Required: CSS selector or DOM element + url: 'https://example.com', // Optional: defaults to window.location.href + title: 'Custom Title', // Optional: defaults to document.title + buttonText: 'Share', // Optional: button label text + buttonStyle: 'primary', // default | primary | compact | icon-only + theme: 'dark', // dark | light + platforms: ['twitter', 'linkedin'], // Optional: defaults to all platforms }); ``` ### All Available Options -| Option | Type | Default | Description | -| ------------------ | -------------- | ---------------------- | -------------------------------------------------- | -| `container` | string/Element | - | **Required.** CSS selector or DOM element | -| `url` | string | `window.location.href` | URL to share | -| `title` | string | `document.title` | Share title/headline | -| `description` | string | `''` | Additional description text | -| `hashtags` | array | `[]` | Hashtags for posts (e.g., `['js', 'webdev']`) | -| `via` | string | `''` | Twitter handle (without @) | -| `platforms` | array | All platforms | Platforms to show (see below) | -| `buttonText` | string | `'Share'` | Button label text | -| `buttonStyle` | string | `'default'` | `default`, `primary`, `compact`, `icon-only` | -| `buttonColor` | string | `''` | Custom button background color | -| `buttonHoverColor` | string | `''` | Custom button hover color | -| `customClass` | string | `''` | Additional CSS class for button | -| `theme` | string | `'dark'` | `dark` or `light` | -| `modalPosition` | string | `'center'` | Modal position on screen | -| `showButton` | boolean | `true` | Show/hide the share button | -| `onShare` | function | `null` | Callback when user shares: `(platform, url) => {}` | -| `onCopy` | function | `null` | Callback when user copies link: `(url) => {}` | +| Option | Type | Default | Description | +| ------------------ | -------------- | ---------------------- | ---------------------------------------------------- | +| `container` | string/Element | - | **Required.** CSS selector or DOM element | +| `url` | string | `window.location.href` | URL to share | +| `title` | string | `document.title` | Share title/headline | +| `description` | string | `''` | Additional description text | +| `hashtags` | array | `[]` | Hashtags for posts (e.g., `['js', 'webdev']`) | +| `via` | string | `''` | Twitter handle (without @) | +| `platforms` | array | All platforms | Platforms to show (see below) | +| `buttonText` | string | `'Share'` | Button label text | +| `buttonStyle` | string | `'default'` | `default`, `primary`, `compact`, `icon-only` | +| `buttonColor` | string | `''` | Custom button background color | +| `buttonHoverColor` | string | `''` | Custom button hover color | +| `customClass` | string | `''` | Additional CSS class for button | +| `theme` | string | `'dark'` | `dark` or `light` | +| `modalPosition` | string | `'center'` | Modal position on screen | +| `showButton` | boolean | `true` | Show/hide the share button | +| `onShare` | function | `null` | Callback when user shares: `(platform, url) => {}` | +| `onCopy` | function | `null` | Callback when user copies link: `(url) => {}` | +| `autoDetect` | boolean | `true` | Auto-detect title/description from page when omitted | **Available Platforms:** `whatsapp`, `facebook`, `twitter`, `linkedin`, `telegram`, `reddit`, `email` @@ -481,12 +558,12 @@ Control the text that appears when users share to social platforms: ```jsx new SocialShareButton({ - container: "#share-button", - url: "https://myproject.com", - title: "Check out my awesome project!", // Main title/headline - description: "An amazing tool for developers", // Additional description - hashtags: ["javascript", "webdev", "opensource"], // Hashtags included in posts - via: "MyProjectHandle", // Your Twitter handle + container: '#share-button', + url: 'https://myproject.com', + title: 'Check out my awesome project!', // Main title/headline + description: 'An amazing tool for developers', // Additional description + hashtags: ['javascript', 'webdev', 'opensource'], // Hashtags included in posts + via: 'MyProjectHandle', // Your Twitter handle }); ``` @@ -506,8 +583,8 @@ new SocialShareButton({ ```jsx new SocialShareButton({ - container: "#share-button", - buttonStyle: "primary", // or 'default', 'compact', 'icon-only' + container: '#share-button', + buttonStyle: 'primary', // or 'default', 'compact', 'icon-only' }); ``` @@ -517,9 +594,9 @@ Pass `buttonColor` and `buttonHoverColor` to match your project's color scheme: ```jsx new SocialShareButton({ - container: "#share-button", - buttonColor: "#ff6b6b", // Button background color - buttonHoverColor: "#ff5252", // Hover state color + container: '#share-button', + buttonColor: '#ff6b6b', // Button background color + buttonHoverColor: '#ff5252', // Hover state color }); ``` @@ -529,9 +606,9 @@ For more complex styling, use a custom CSS class: ```jsx new SocialShareButton({ - container: "#share-button", - buttonStyle: "primary", - customClass: "my-custom-button", + container: '#share-button', + buttonStyle: 'primary', + customClass: 'my-custom-button', }); ``` @@ -555,23 +632,23 @@ Then in your CSS file: ```jsx // Material Design Red new SocialShareButton({ - container: "#share-button", - buttonColor: "#f44336", - buttonHoverColor: "#da190b", + container: '#share-button', + buttonColor: '#f44336', + buttonHoverColor: '#da190b', }); // Tailwind Blue new SocialShareButton({ - container: "#share-button", - buttonColor: "#3b82f6", - buttonHoverColor: "#2563eb", + container: '#share-button', + buttonColor: '#3b82f6', + buttonHoverColor: '#2563eb', }); // Custom Brand Color new SocialShareButton({ - container: "#share-button", - buttonColor: "#your-brand-color", - buttonHoverColor: "#your-brand-color-dark", + container: '#share-button', + buttonColor: '#your-brand-color', + buttonHoverColor: '#your-brand-color-dark', }); ``` @@ -588,12 +665,12 @@ new SocialShareButton({ ```jsx new SocialShareButton({ - container: "#share-button", + container: '#share-button', onShare: (platform, url) => { console.log(`Shared on ${platform}: ${url}`); }, onCopy: (url) => { - console.log("Link copied:", url); + console.log('Link copied:', url); }, }); ``` @@ -605,11 +682,11 @@ new SocialShareButton({ ### Using npm Package ```javascript -import SocialShareButton from "social-share-button-aossie"; -import "social-share-button-aossie/src/social-share-button.css"; +import SocialShareButton from 'social-share-button-aossie'; +import 'social-share-button-aossie/src/social-share-button.css'; new SocialShareButton({ - container: "#share-button", + container: '#share-button', }); ``` @@ -618,10 +695,10 @@ new SocialShareButton({ If you want a reusable React component, copy `src/social-share-button-react.jsx` to your project: ```jsx -import { SocialShareButton } from "./components/SocialShareButton"; +import { SocialShareButton } from './components/SocialShareButton'; function App() { - return ; + return ; } ``` @@ -640,7 +717,7 @@ const shareButton = useRef(null); useEffect(() => { shareButton.current = new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); }, []); @@ -676,7 +753,7 @@ useEffect(() => { ```jsx if (window.SocialShareButton) { - new window.SocialShareButton({ container: "#share-button" }); + new window.SocialShareButton({ container: '#share-button' }); } ``` @@ -729,14 +806,14 @@ if (window.SocialShareButton) { ```jsx // Professional networks only new SocialShareButton({ - container: "#share-button", - platforms: ["linkedin", "twitter", "email"], + container: '#share-button', + platforms: ['linkedin', 'twitter', 'email'], }); // Messaging apps only new SocialShareButton({ - container: "#share-button", - platforms: ["whatsapp", "telegram"], + container: '#share-button', + platforms: ['whatsapp', 'telegram'], }); ``` @@ -744,9 +821,9 @@ new SocialShareButton({ ```jsx new SocialShareButton({ - container: "#share-button", - buttonStyle: "icon-only", - theme: "light", + container: '#share-button', + buttonStyle: 'icon-only', + theme: 'light', }); ``` diff --git a/package-lock.json b/package-lock.json index cc856f2..d1790bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,9 +12,204 @@ "@eslint/js": "^9.39.4", "eslint": "^9.39.4", "globals": "^17.4.0", + "jsdom": "^29.0.0", "prettier": "^3.2.4" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.3.tgz", + "integrity": "sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz", + "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -159,6 +354,24 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -295,6 +508,16 @@ "dev": true, "license": "MIT" }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -375,6 +598,34 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -393,6 +644,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -400,6 +658,19 @@ "dev": true, "license": "MIT" }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -701,6 +972,19 @@ "node": ">=8" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -761,6 +1045,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -781,6 +1072,47 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.0.tgz", + "integrity": "sha512-9FshNB6OepopZ08unmmGpsF7/qCjxGPbo3NbgfJAnPeHXnsODE9WWffXZtRFRFe0ntzaAOcSKNJFz8wiyvF1jQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@asamuzakjp/dom-selector": "^7.0.2", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.24.3", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -849,6 +1181,23 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -939,6 +1288,19 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -995,6 +1357,16 @@ "node": ">=6" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -1005,6 +1377,19 @@ "node": ">=4" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -1028,6 +1413,16 @@ "node": ">=8" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -1054,6 +1449,59 @@ "node": ">=8" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tldts": { + "version": "7.0.25", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.25.tgz", + "integrity": "sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.25" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.25", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.25.tgz", + "integrity": "sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -1067,6 +1515,16 @@ "node": ">= 0.8.0" } }, + "node_modules/undici": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.3.tgz", + "integrity": "sha512-eJdUmK/Wrx2d+mnWWmwwLRyA7OQCkLap60sk3dOK4ViZR7DKwwptwuIvFBg2HaiP9ESaEdhtpSymQPvytpmkCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -1077,6 +1535,54 @@ "punycode": "^2.1.0" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -1103,6 +1609,23 @@ "node": ">=0.10.0" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index c0fc830..0deb867 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "src/social-share-button.css", "src/social-share-button-react.jsx", "src/social-share-analytics.js", + "src/utils/extractContent.js", "README.md", "LICENSE" ], @@ -15,7 +16,8 @@ "lint": "eslint src/**/*.{js,jsx} --max-warnings=0", "lint:fix": "eslint src/**/*.{js,jsx} --fix", "format": "prettier --write \"**/*.{js,jsx,json,css,md,html}\"", - "format:check": "prettier --check \"**/*.{js,jsx,json,css,md,html}\"" + "format:check": "prettier --check \"**/*.{js,jsx,json,css,md,html}\"", + "test": "node --test tests/**/*.test.js" }, "repository": { "type": "git", @@ -44,6 +46,7 @@ "@eslint/js": "^9.39.4", "eslint": "^9.39.4", "globals": "^17.4.0", + "jsdom": "^29.0.0", "prettier": "^3.2.4" }, "type": "module" diff --git a/src/social-share-button-react.jsx b/src/social-share-button-react.jsx index 43bf022..8b1db93 100644 --- a/src/social-share-button-react.jsx +++ b/src/social-share-button-react.jsx @@ -1,46 +1,41 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef } from 'react'; export const SocialShareButton = ({ url, title, - description = "", + description = '', hashtags = [], - via = "", - platforms = [ - "whatsapp", - "facebook", - "twitter", - "linkedin", - "telegram", - "reddit", - ], - theme = "dark", - buttonText = "Share", - customClass = "", + via = '', + platforms = ['whatsapp', 'facebook', 'twitter', 'linkedin', 'telegram', 'reddit'], + theme = 'dark', + buttonText = 'Share', + customClass = '', onShare = null, onCopy = null, - buttonStyle = "default", - modalPosition = "center", + buttonStyle = 'default', + modalPosition = 'center', + // Content auto-detection — set to false when all props are always provided. + autoDetect = true, // Analytics props — the library itself never collects data. // Provide any combination to connect your own analytics tools. - analytics = true, // set to false to disable all event emission - onAnalytics = null, // (payload) => void — direct callback hook - analyticsPlugins = [], // array of adapter instances (see social-share-analytics.js) - componentId = null, // optional string identifier for this instance - debug = false, // log events to console during development + analytics = true, // set to false to disable all event emission + onAnalytics = null, // (payload) => void — direct callback hook + analyticsPlugins = [], // array of adapter instances (see social-share-analytics.js) + componentId = null, // optional string identifier for this instance + debug = false, // log events to console during development }) => { const containerRef = useRef(null); const shareButtonRef = useRef(null); - // Auto-detect current URL and title if not provided - const currentUrl = - url || (typeof window !== "undefined" ? window.location.href : ""); - const currentTitle = - title || (typeof document !== "undefined" ? document.title : ""); + // Auto-detect current URL and title if not provided. + // When autoDetect is enabled, the vanilla SocialShareButton constructor + // handles deeper detection (og:title, meta description, semantic HTML). + const currentUrl = url || (typeof window !== 'undefined' ? window.location.href : ''); + const currentTitle = title || (typeof document !== 'undefined' ? document.title : ''); useEffect(() => { if (containerRef.current && !shareButtonRef.current) { - if (typeof window !== "undefined" && window.SocialShareButton) { + if (typeof window !== 'undefined' && window.SocialShareButton) { shareButtonRef.current = new window.SocialShareButton({ container: containerRef.current, url: currentUrl, @@ -56,6 +51,7 @@ export const SocialShareButton = ({ onCopy, buttonStyle, modalPosition, + autoDetect, analytics, onAnalytics, analyticsPlugins, @@ -73,9 +69,21 @@ export const SocialShareButton = ({ }; }, []); - // Update options when props change (including URL from route changes) + // Update options when props change (including URL from route changes). + // Also bust the content-detection cache so the new page's metadata is used. useEffect(() => { if (shareButtonRef.current) { + // Invalidate detection cache on every route/prop change so the new + // page content is picked up when autoDetect is enabled. + if ( + autoDetect && + typeof window !== 'undefined' && + window.SocialShareButton && + typeof window.SocialShareButton.clearContentCache === 'function' + ) { + window.SocialShareButton.clearContentCache(); + } + shareButtonRef.current.updateOptions({ url: currentUrl, title: currentTitle, @@ -90,6 +98,7 @@ export const SocialShareButton = ({ onCopy, buttonStyle, modalPosition, + autoDetect, analytics, onAnalytics, analyticsPlugins, @@ -111,6 +120,7 @@ export const SocialShareButton = ({ onCopy, buttonStyle, modalPosition, + autoDetect, analytics, onAnalytics, analyticsPlugins, diff --git a/src/social-share-button.js b/src/social-share-button.js index 5f05397..e7c0b23 100644 --- a/src/social-share-button.js +++ b/src/social-share-button.js @@ -4,40 +4,221 @@ * @license GPL-3.0 */ +/** + * Lightweight content auto-detection module (inlined to preserve zero-dependency CDN build). + * + * Detection priority for title: + * og:title → twitter:title → semantic h1 (article/main/landmark) → document.title + * + * Detection priority for description / excerpt: + * og:description → twitter:description → meta[name="description"] → body text excerpt + * + * Results are cached for 30 s per instance so repeated calls within the same + * page lifecycle are negligible in cost. + */ +const _ContentDetector = (() => { + let _cache = null; + const CACHE_TTL_MS = 30_000; + + function _getMeta(doc, selector) { + const el = doc.querySelector(selector); + return el && el.getAttribute('content') ? el.getAttribute('content').trim() : ''; + } + + function _detectTitle(doc) { + const og = _getMeta(doc, 'meta[property="og:title"]'); + if (og) return og; + + const tw = + _getMeta(doc, 'meta[name="twitter:title"]') || + _getMeta(doc, 'meta[property="twitter:title"]'); + if (tw) return tw; + + const landmarkSelectors = [ + 'article h1', + '[role="main"] h1', + 'main h1', + '.post-title', + '.entry-title', + '.article-title', + '.page-title', + '.hero-title', + 'h1', + ]; + for (const sel of landmarkSelectors) { + const el = doc.querySelector(sel); + if (el) { + const text = el.textContent.trim(); + if (text) return text; + } + } + + // Strip trailing " | SiteName" suffix from document.title + const raw = (doc.title || '').trim(); + return raw.replace(/\s*[|\-\u2013\u2014]\s*.{1,60}$/, '').trim() || raw; + } + + function _detectMetaDesc(doc) { + return ( + _getMeta(doc, 'meta[property="og:description"]') || + _getMeta(doc, 'meta[name="twitter:description"]') || + _getMeta(doc, 'meta[property="twitter:description"]') || + _getMeta(doc, 'meta[name="description"]') + ); + } + + function _findContentRoot(doc) { + const candidates = [ + 'article', + '[role="main"]', + 'main', + '.post-content', + '.entry-content', + '.article-content', + '.article-body', + '.blog-content', + '.page-content', + '.content-body', + '#content', + '#main-content', + ]; + for (const sel of candidates) { + const el = doc.querySelector(sel); + if (el && el.textContent.trim().length > 50) return el; + } + return doc.body || doc.documentElement; + } + + function _toPlainText(root) { + const clone = root.cloneNode(true); + clone + .querySelectorAll( + 'script,style,noscript,nav,header,footer,aside,form,' + + "[aria-hidden='true'],.nav,.navigation,.menu,.sidebar," + + '.advertisement,.ad,.cookie-banner,.social-share-modal-overlay' + ) + .forEach((el) => el.remove()); + return clone.textContent.replace(/\s+/g, ' ').trim(); + } + + function _excerpt(text, minLen = 140, maxLen = 200) { + if (!text) return ''; + if (text.length <= maxLen) return text; + const win = text.slice(0, maxLen); + const sentEnd = Math.max(win.lastIndexOf('. '), win.lastIndexOf('! '), win.lastIndexOf('? ')); + if (sentEnd >= minLen) return win.slice(0, sentEnd + 1).trim(); + const wordEnd = win.lastIndexOf(' '); + return wordEnd > minLen ? win.slice(0, wordEnd).trim() + '\u2026' : win.trim() + '\u2026'; + } + + return { + /** + * Extract sharing metadata from the document. + * Returns { title, excerpt, textContent }. + * Never throws — falls back gracefully on any DOM error. + * + * @param {Document} doc + * @param {boolean} [bustCache=false] + * @returns {{ title: string, excerpt: string, textContent: string }} + */ + extract(doc, bustCache = false) { + if (!bustCache && _cache && Date.now() - _cache.ts < CACHE_TTL_MS) { + return _cache.result; + } + let result; + try { + const title = _detectTitle(doc); + let excerpt = _detectMetaDesc(doc); + let textContent = ''; + if (!excerpt) { + const root = _findContentRoot(doc); + textContent = _toPlainText(root); + excerpt = _excerpt(textContent); + } else { + try { + textContent = _toPlainText(_findContentRoot(doc)); + } catch (_) { + textContent = excerpt; + } + } + result = { title, excerpt, textContent }; + } catch (_) { + result = { + title: typeof doc !== 'undefined' && doc.title ? doc.title.trim() : '', + excerpt: '', + textContent: '', + }; + } + _cache = { result, ts: Date.now() }; + return result; + }, + + /** Clear extraction cache (call on SPA navigation). */ + clearCache() { + _cache = null; + }, + }; +})(); + /** Analytics event schema version. Increment when the payload shape changes. */ -const ANALYTICS_SCHEMA_VERSION = "1.0"; +const ANALYTICS_SCHEMA_VERSION = '1.0'; class SocialShareButton { constructor(options = {}) { + // ------------------------------------------------------------------------- + // Intelligent content auto-detection (Issue #26) + // + // When title or description are not provided by the caller, attempt to + // detect them from the page's metadata and semantic HTML. Manual props + // always win — auto-detection only fills in what's missing. + // + // Set autoDetect: false to disable entirely. + // ------------------------------------------------------------------------- + let _autoTitle = ''; + let _autoDescription = ''; + + const _autoDetectEnabled = options.autoDetect !== false; + + if (_autoDetectEnabled && typeof document !== 'undefined') { + const _needsTitle = !options.title; + const _needsDesc = !options.description; + + if (_needsTitle || _needsDesc) { + try { + const detected = _ContentDetector.extract(document); + if (_needsTitle) _autoTitle = detected.title || ''; + if (_needsDesc) _autoDescription = detected.excerpt || ''; + } catch (_) { + // Never let detection errors break the constructor + } + } + } + this.options = { - url: - options.url || - (typeof window !== "undefined" ? window.location.href : ""), - title: - options.title || - (typeof document !== "undefined" ? document.title : ""), - description: options.description || "", + url: options.url || (typeof window !== 'undefined' ? window.location.href : ''), + title: options.title || _autoTitle || (typeof document !== 'undefined' ? document.title : ''), + description: options.description || _autoDescription || '', hashtags: options.hashtags || [], - via: options.via || "", + via: options.via || '', platforms: options.platforms || [ - "whatsapp", - "facebook", - "twitter", - "linkedin", - "telegram", - "reddit", + 'whatsapp', + 'facebook', + 'twitter', + 'linkedin', + 'telegram', + 'reddit', ], - theme: options.theme || "dark", - buttonText: options.buttonText || "Share", - customClass: options.customClass || "", - buttonColor: options.buttonColor || "", - buttonHoverColor: options.buttonHoverColor || "", + 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", + 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 @@ -45,6 +226,8 @@ class SocialShareButton { 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 + // Content auto-detection — set to false to disable (e.g. when all props are always provided) + autoDetect: _autoDetectEnabled, }; this.isModalOpen = false; @@ -55,14 +238,13 @@ class SocialShareButton { this.handleKeydown = null; this.listeners = []; // Central registry for all event listeners - this.openTimeout = null; // Track setTimeout for openModal animation + 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) - if (this.options.container) { this.init(); } @@ -78,9 +260,9 @@ class SocialShareButton { } createButton() { - const button = document.createElement("button"); + const button = document.createElement('button'); button.className = `social-share-btn ${this.options.buttonStyle} ${this.options.customClass}`; - button.setAttribute("aria-label", "Share"); + button.setAttribute('aria-label', 'Share'); button.innerHTML = ` + +
+

🤖 Content Auto-Detection

+

Automatically detects page title and extracts descriptions from meta tags or page content. Zero config required!

+ +
+ + + +<!-- Include via CDN --> +<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/AOSSIE-Org/SocialShareButton@feature/socialshare-autodetect/src/social-share-button.css"> +<script src="https://cdn.jsdelivr.net/gh/AOSSIE-Org/SocialShareButton@feature/socialshare-autodetect/src/social-share-button.js"></script> + +<div id="demo-autodetect"></div> + +<script> + // Initialize with zero config for auto-detection + new SocialShareButton({ + container: '#demo-autodetect' + }); +</script> +
+ +
+
+

Auto-Detect (Enabled)

+

Title and description automatically populated

+
+
+
+

Auto-Detect (Disabled)

+

Manual props overriding auto-detection

+
+
+
+
+

🚀 Quick Start

@@ -546,6 +583,21 @@

Ready to Get Started?

platforms: ['whatsapp', 'telegram'], buttonText: 'Share' }); + // Auto-Detect Enabled + new SocialShareButton({ + container: '#demo-autodetect', + buttonText: 'Auto-Detect Share' + }); + + // Auto-Detect Disabled + new SocialShareButton({ + container: '#demo-no-autodetect', + autoDetect: false, + title: 'Manual Title Override', + description: 'This uses explicit manual props instead of auto-detection.', + buttonText: 'Manual Share' + }); + // Custom Colors - Red new SocialShareButton({ diff --git a/package-lock.json b/package-lock.json index d1790bb..f3b84a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,68 +12,28 @@ "@eslint/js": "^9.39.4", "eslint": "^9.39.4", "globals": "^17.4.0", - "jsdom": "^29.0.0", + "jsdom": "^24.0.0", "prettier": "^3.2.4" } }, "node_modules/@asamuzakjp/css-color": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", - "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", "dev": true, "license": "MIT", "dependencies": { - "@csstools/css-calc": "^3.1.1", - "@csstools/css-color-parser": "^4.0.2", - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.2.6" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.3.tgz", - "integrity": "sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", - "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.7" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@bramus/specificity": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "^3.0.0" - }, - "bin": { - "specificity": "bin/cli.js" + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" } }, "node_modules/@csstools/color-helpers": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", - "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", "dev": true, "funding": [ { @@ -87,13 +47,13 @@ ], "license": "MIT-0", "engines": { - "node": ">=20.19.0" + "node": ">=18" } }, "node_modules/@csstools/css-calc": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", - "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", "dev": true, "funding": [ { @@ -107,17 +67,17 @@ ], "license": "MIT", "engines": { - "node": ">=20.19.0" + "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-color-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", - "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", "dev": true, "funding": [ { @@ -131,21 +91,21 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^6.0.2", - "@csstools/css-calc": "^3.1.1" + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" }, "engines": { - "node": ">=20.19.0" + "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", - "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", "dev": true, "funding": [ { @@ -158,42 +118,18 @@ } ], "license": "MIT", + "peer": true, "engines": { - "node": ">=20.19.0" + "node": ">=18" }, "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz", - "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "peerDependencies": { - "css-tree": "^3.2.1" - }, - "peerDependenciesMeta": { - "css-tree": { - "optional": true - } + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-tokenizer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", - "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", "dev": true, "funding": [ { @@ -206,8 +142,9 @@ } ], "license": "MIT", + "peer": true, "engines": { - "node": ">=20.19.0" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -354,24 +291,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@exodus/bytes": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", - "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -444,6 +363,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -461,6 +381,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -501,6 +431,13 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -508,16 +445,6 @@ "dev": true, "license": "MIT" }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -529,6 +456,20 @@ "concat-map": "0.0.1" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -576,6 +517,19 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -598,32 +552,39 @@ "node": ">= 8" } }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", "dev": true, "license": "MIT", "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + "node": ">=18" } }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/data-urls": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, "license": "MIT", "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": ">=18" } }, "node_modules/debug": { @@ -658,6 +619,31 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -671,6 +657,55 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -690,6 +725,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -936,6 +972,72 @@ "dev": true, "license": "ISC" }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -962,6 +1064,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -972,17 +1087,100 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "license": "MIT", "dependencies": { - "@exodus/bytes": "^1.6.0" + "whatwg-encoding": "^3.1.1" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/ignore": { @@ -1073,39 +1271,39 @@ } }, "node_modules/jsdom": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.0.tgz", - "integrity": "sha512-9FshNB6OepopZ08unmmGpsF7/qCjxGPbo3NbgfJAnPeHXnsODE9WWffXZtRFRFe0ntzaAOcSKNJFz8wiyvF1jQ==", + "version": "24.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.0.0.tgz", + "integrity": "sha512-UDS2NayCvmXSXVP6mpTj+73JnNQadZlr9N68189xib2tx5Mls7swlTNao26IoHv46BZJFvXygyRtyXd1feAk1A==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^5.0.1", - "@asamuzakjp/dom-selector": "^7.0.2", - "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.1", - "@exodus/bytes": "^1.15.0", - "css-tree": "^3.2.1", - "data-urls": "^7.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.7", - "parse5": "^8.0.0", + "nwsapi": "^2.2.7", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.1", - "undici": "^7.24.3", + "tough-cookie": "^4.1.3", "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.1", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.16.0", "xml-name-validator": "^5.0.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + "node": ">=18" }, "peerDependencies": { - "canvas": "^3.0.0" + "canvas": "^2.11.2" }, "peerDependenciesMeta": { "canvas": { @@ -1182,21 +1380,44 @@ "license": "MIT" }, "node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "engines": { - "node": "20 || >=22" + "node": ">= 0.6" } }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, - "license": "CC0-1.0" + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } }, "node_modules/minimatch": { "version": "3.1.5", @@ -1225,6 +1446,13 @@ "dev": true, "license": "MIT" }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -1289,9 +1517,9 @@ } }, "node_modules/parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "license": "MIT", "dependencies": { @@ -1347,6 +1575,19 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -1357,15 +1598,19 @@ "node": ">=6" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" }, "node_modules/resolve-from": { "version": "4.0.0", @@ -1377,6 +1622,20 @@ "node": ">=4" } }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -1413,16 +1672,6 @@ "node": ">=8" } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -1456,50 +1705,33 @@ "dev": true, "license": "MIT" }, - "node_modules/tldts": { - "version": "7.0.25", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.25.tgz", - "integrity": "sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^7.0.25" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.0.25", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.25.tgz", - "integrity": "sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==", - "dev": true, - "license": "MIT" - }, "node_modules/tough-cookie": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", - "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "tldts": "^7.0.5" + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "engines": { - "node": ">=16" + "node": ">=6" } }, "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "dev": true, "license": "MIT", "dependencies": { "punycode": "^2.3.1" }, "engines": { - "node": ">=20" + "node": ">=18" } }, "node_modules/type-check": { @@ -1515,14 +1747,14 @@ "node": ">= 0.8.0" } }, - "node_modules/undici": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.3.tgz", - "integrity": "sha512-eJdUmK/Wrx2d+mnWWmwwLRyA7OQCkLap60sk3dOK4ViZR7DKwwptwuIvFBg2HaiP9ESaEdhtpSymQPvytpmkCA==", + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true, "license": "MIT", "engines": { - "node": ">=20.18.1" + "node": ">= 4.0.0" } }, "node_modules/uri-js": { @@ -1535,6 +1767,17 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -1549,38 +1792,51 @@ } }, "node_modules/webidl-conversions": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=20" + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" } }, "node_modules/whatwg-mimetype": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", "engines": { - "node": ">=20" + "node": ">=18" } }, "node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "dev": true, "license": "MIT", "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": ">=18" } }, "node_modules/which": { @@ -1609,6 +1865,28 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", diff --git a/package.json b/package.json index 7a92398..a9c0eaf 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@eslint/js": "^9.39.4", "eslint": "^9.39.4", "globals": "^17.4.0", - "jsdom": "^28.1.0", + "jsdom": "^24.0.0", "prettier": "^3.2.4" }, "type": "module" From ee0366bce8842d82069d5391f166429d6dd8b4e8 Mon Sep 17 00:00:00 2001 From: Muneerali199 Date: Wed, 18 Mar 2026 13:47:32 +0530 Subject: [PATCH 16/16] chore: format files --- .github/copilot/integrate-analytics.prompt.md | 57 ++++--- .../integrate-social-share-button.prompt.md | 152 +++++++++--------- docs/client-guide.md | 10 +- eslint.config.js | 20 +-- src/social-share-analytics.js | 34 ++-- src/social-share-button.css | 12 +- 6 files changed, 137 insertions(+), 148 deletions(-) diff --git a/.github/copilot/integrate-analytics.prompt.md b/.github/copilot/integrate-analytics.prompt.md index 60cd595..cc8f4c6 100644 --- a/.github/copilot/integrate-analytics.prompt.md +++ b/.github/copilot/integrate-analytics.prompt.md @@ -41,14 +41,14 @@ forwards to whatever tool they choose. ## 2 — Core events catalogue -| `eventName` | `interactionType` | Fires when | -| ---------------------------- | ----------------- | ----------------------------------------------- | -| `social_share_popup_open` | `popup_open` | Share modal/popup opens | -| `social_share_popup_close` | `popup_close` | Modal closes (button, overlay, or Esc key) | -| `social_share_click` | `share` | User clicks a platform button (share intent) | -| `social_share_success` | `share` | Platform share window opened successfully | -| `social_share_copy` | `copy` | User copies the link to clipboard | -| `social_share_error` | `error` | Share or copy action failed | +| `eventName` | `interactionType` | Fires when | +| -------------------------- | ----------------- | -------------------------------------------- | +| `social_share_popup_open` | `popup_open` | Share modal/popup opens | +| `social_share_popup_close` | `popup_close` | Modal closes (button, overlay, or Esc key) | +| `social_share_click` | `share` | User clicks a platform button (share intent) | +| `social_share_success` | `share` | Platform share window opened successfully | +| `social_share_copy` | `copy` | User copies the link to clipboard | +| `social_share_error` | `error` | Share or copy action failed | --- @@ -59,7 +59,7 @@ forwards to whatever tool they choose. ```js // Fires on the container element and bubbles through the DOM (composed:true // means it also crosses shadow-DOM boundaries). -document.addEventListener("social-share", (e) => { +document.addEventListener('social-share', (e) => { const payload = e.detail; // Forward to your analytics tool here }); @@ -72,7 +72,7 @@ and Mixpanel both need the same event. ```js new SocialShareButton({ - container: "#share-button", + container: '#share-button', onAnalytics: (payload) => { // Forward to your analytics tool here }, @@ -96,11 +96,8 @@ Load the adapters file **in addition to** the main library script: const { GoogleAnalyticsAdapter, MixpanelAdapter } = window.SocialShareAnalytics; new SocialShareButton({ - container: "#share-button", - analyticsPlugins: [ - new GoogleAnalyticsAdapter(), - new MixpanelAdapter(), - ], + container: '#share-button', + analyticsPlugins: [new GoogleAnalyticsAdapter(), new MixpanelAdapter()], }); ``` @@ -125,7 +122,7 @@ Prerequisite: GA4 `gtag.js` snippet loaded by the host. const { GoogleAnalyticsAdapter } = window.SocialShareAnalytics; new SocialShareButton({ - container: "#share-button", + container: '#share-button', analyticsPlugins: [new GoogleAnalyticsAdapter()], }); @@ -135,7 +132,7 @@ new SocialShareButton({ Custom event category (optional): ```js -new GoogleAnalyticsAdapter({ eventCategory: "engagement" }) +new GoogleAnalyticsAdapter({ eventCategory: 'engagement' }); ``` ### Mixpanel @@ -145,7 +142,7 @@ Prerequisite: `mixpanel-browser` snippet or SDK loaded. ```js const { MixpanelAdapter } = window.SocialShareAnalytics; new SocialShareButton({ - container: "#share-button", + container: '#share-button', analyticsPlugins: [new MixpanelAdapter()], }); // Calls: mixpanel.track(eventName, { platform, url, ... }) @@ -156,7 +153,7 @@ new SocialShareButton({ ```js const { SegmentAdapter } = window.SocialShareAnalytics; new SocialShareButton({ - container: "#share-button", + container: '#share-button', analyticsPlugins: [new SegmentAdapter()], }); // Calls: analytics.track(eventName, { platform, url, ... }) @@ -169,7 +166,7 @@ Prerequisite: Plausible `script.js` loaded with custom events enabled. ```js const { PlausibleAdapter } = window.SocialShareAnalytics; new SocialShareButton({ - container: "#share-button", + container: '#share-button', analyticsPlugins: [new PlausibleAdapter()], }); // Calls: plausible(eventName, { props: { platform, url, ... } }) @@ -180,7 +177,7 @@ new SocialShareButton({ ```js const { PostHogAdapter } = window.SocialShareAnalytics; new SocialShareButton({ - container: "#share-button", + container: '#share-button', analyticsPlugins: [new PostHogAdapter()], }); // Calls: posthog.capture(eventName, { platform, url, ... }) @@ -195,8 +192,8 @@ const { CustomAdapter } = window.SocialShareAnalytics; new SocialShareButton({ analyticsPlugins: [ new CustomAdapter((payload) => { - fetch("/api/analytics", { - method: "POST", + fetch('/api/analytics', { + method: 'POST', body: JSON.stringify(payload), }); }), @@ -231,17 +228,17 @@ GA4, Mixpanel, and a custom endpoint at the same time: ```js const { GoogleAnalyticsAdapter, MixpanelAdapter, CustomAdapter } = window.SocialShareAnalytics; -document.addEventListener("social-share", (e) => { - console.log("Raw event:", e.detail); // Debugging / logging +document.addEventListener('social-share', (e) => { + console.log('Raw event:', e.detail); // Debugging / logging }); new SocialShareButton({ - container: "#share-button", - componentId: "homepage-hero", + container: '#share-button', + componentId: 'homepage-hero', analyticsPlugins: [ new GoogleAnalyticsAdapter(), new MixpanelAdapter(), - new CustomAdapter((p) => fetch("/log", { method: "POST", body: JSON.stringify(p) })), + new CustomAdapter((p) => fetch('/log', { method: 'POST', body: JSON.stringify(p) })), ], }); ``` @@ -255,7 +252,7 @@ development. Remove or set to `false` in production. ```js new SocialShareButton({ - container: "#share-button", + container: '#share-button', debug: true, // → [SocialShareButton Analytics] { version: '1.0', source: 'social-share-button', ... } }); @@ -271,7 +268,7 @@ instrumentation must be explicitly consented to before activation. ```js new SocialShareButton({ - container: "#share-button", + container: '#share-button', analytics: false, }); ``` diff --git a/.github/copilot/integrate-social-share-button.prompt.md b/.github/copilot/integrate-social-share-button.prompt.md index 3b7a24b..ffd571d 100644 --- a/.github/copilot/integrate-social-share-button.prompt.md +++ b/.github/copilot/integrate-social-share-button.prompt.md @@ -25,11 +25,11 @@ You are helping a developer integrate the **SocialShareButton** library The README defines **3 installation methods**. Ask (or infer) which the developer wants: -| Method | When to use | -|--------|-------------| -| **Method 1 — CDN (Recommended)** | Most projects. No build step needed. Load via ` ``` @@ -99,8 +102,8 @@ No framework. Just add the CDN tags directly: **Step 2:** Open an **existing** component that renders on every page — typically `src/components/Header.jsx`, `src/layouts/MainLayout.jsx`, or your root `App.jsx`. Add the snippet below to that component so the share button is consistently available across your app. ```jsx -import { useEffect, useRef } from "react"; -import { useLocation } from "react-router-dom"; // omit if not using React Router +import { useEffect, useRef } from 'react'; +import { useLocation } from 'react-router-dom'; // omit if not using React Router // ⬇️ Replace 'Header' with the name of the component where you want the // share button to appear — e.g. Navbar, MainLayout, App, etc. @@ -113,7 +116,7 @@ function Header() { if (initRef.current || !window.SocialShareButton) return; shareButtonRef.current = new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); initRef.current = true; @@ -150,13 +153,9 @@ function Header() { **Step 1:** Add CDN to `app/layout.tsx`: ```tsx -import Script from "next/script"; +import Script from 'next/script'; -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( @@ -180,10 +179,10 @@ export default function RootLayout({ **Step 2:** Because `SocialShareButton` manipulates the DOM, it must run inside a **Client Component** (note the `"use client"` directive at the top). Add the snippet below to an existing component such as `app/components/Header.tsx` or `app/components/Navbar.tsx` — any component already included in your layout. ```tsx -"use client"; +'use client'; -import { useEffect, useRef } from "react"; -import { usePathname } from "next/navigation"; +import { useEffect, useRef } from 'react'; +import { usePathname } from 'next/navigation'; // ⬇️ Replace 'Header' with the name of the component where you want the // share button to appear — e.g. Navbar, MainLayout, App, etc. @@ -195,11 +194,10 @@ export default function Header() { useEffect(() => { const initButton = () => { - if (initRef.current || !window.SocialShareButton || !containerRef.current) - return; + if (initRef.current || !window.SocialShareButton || !containerRef.current) return; shareButtonRef.current = new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); initRef.current = true; }; @@ -262,7 +260,7 @@ declare global { **Step 1:** Add CDN to `pages/_document.tsx`: ```tsx -import { Html, Head, Main, NextScript } from "next/document"; +import { Html, Head, Main, NextScript } from 'next/document'; export default function Document() { return ( @@ -286,8 +284,8 @@ export default function Document() { **Step 2:** Open an existing component that is rendered on every page — typically `components/Header.tsx`, `components/Navbar.tsx`, or `components/Layout.tsx`. Since `_document.tsx` loads the script globally, the button is ready to initialize in any of these components. ```tsx -import { useEffect, useRef } from "react"; -import { useRouter } from "next/router"; +import { useEffect, useRef } from 'react'; +import { useRouter } from 'next/router'; // ⬇️ Replace 'Header' with the name of the component where you want the // share button to appear — e.g. Navbar, MainLayout, App, etc. @@ -299,11 +297,10 @@ export default function Header() { useEffect(() => { const initButton = () => { - if (initRef.current || !window.SocialShareButton || !containerRef.current) - return; + if (initRef.current || !window.SocialShareButton || !containerRef.current) return; shareButtonRef.current = new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); initRef.current = true; }; @@ -384,7 +381,7 @@ declare global { // Add
to your component's template/HTML first, // then initialize once the DOM is ready (e.g., in mounted(), ngAfterViewInit(), or useEffect()): new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); ``` @@ -395,10 +392,10 @@ new window.SocialShareButton({ Use when the project has a bundler (Webpack, Vite, etc.) and the developer prefers `import` syntax. Works in any framework. ```javascript -import SocialShareButton from "social-share-button-aossie"; -import "social-share-button-aossie/src/social-share-button.css"; +import SocialShareButton from 'social-share-button-aossie'; +import 'social-share-button-aossie/src/social-share-button.css'; -new SocialShareButton({ container: "#share-button" }); +new SocialShareButton({ container: '#share-button' }); ``` > No CDN tags needed — the npm package includes both JS and CSS. @@ -412,10 +409,10 @@ Only use this when the developer **explicitly** wants a reusable JSX component. Tell them to copy `src/social-share-button-react.jsx` from the library into their project — **do not create a new file from scratch**. ```jsx -import { SocialShareButton } from "./components/SocialShareButton"; +import { SocialShareButton } from './components/SocialShareButton'; function App() { - return ; + return ; } ``` @@ -423,30 +420,30 @@ function App() { ## All constructor options -| Option | Type | Default | Description | -| ------------------ | -------------- | ---------------------- | -------------------------------------------------- | -| `container` | string/Element | — | **Required.** CSS selector or DOM element | -| `url` | string | `window.location.href` | URL to share | -| `title` | string | `document.title` | Share title/headline | -| `description` | string | `''` | Additional description text | -| `hashtags` | array | `[]` | e.g. `['js', 'webdev']` | -| `via` | string | `''` | Twitter handle (without @) | +| Option | Type | Default | Description | +| ------------------ | -------------- | ---------------------- | ---------------------------------------------------------- | +| `container` | string/Element | — | **Required.** CSS selector or DOM element | +| `url` | string | `window.location.href` | URL to share | +| `title` | string | `document.title` | Share title/headline | +| `description` | string | `''` | Additional description text | +| `hashtags` | array | `[]` | e.g. `['js', 'webdev']` | +| `via` | string | `''` | Twitter handle (without @) | | `platforms` | array | All platforms | `whatsapp facebook twitter linkedin telegram reddit email` | -| `buttonText` | string | `'Share'` | Button label text | -| `buttonStyle` | string | `'default'` | `default` `primary` `compact` `icon-only` | -| `buttonColor` | string | `''` | Custom button background color | -| `buttonHoverColor` | string | `''` | Custom button hover color | -| `customClass` | string | `''` | Additional CSS class for button | -| `theme` | string | `'dark'` | `dark` or `light` | -| `modalPosition` | string | `'center'` | Modal position on screen | -| `showButton` | boolean | `true` | Show/hide the share button | -| `onShare` | function | `null` | `(platform, url) => void` | -| `onCopy` | function | `null` | `(url) => void` | -| `analytics` | boolean | `true` | Set `false` to disable all event emission | -| `onAnalytics` | function | `null` | `(payload) => void` — direct analytics hook | -| `analyticsPlugins` | array | `[]` | Adapter instances from `social-share-analytics.js` | -| `componentId` | string | `null` | Label this instance for analytics tracking | -| `debug` | boolean | `false` | Log analytics events to console | +| `buttonText` | string | `'Share'` | Button label text | +| `buttonStyle` | string | `'default'` | `default` `primary` `compact` `icon-only` | +| `buttonColor` | string | `''` | Custom button background color | +| `buttonHoverColor` | string | `''` | Custom button hover color | +| `customClass` | string | `''` | Additional CSS class for button | +| `theme` | string | `'dark'` | `dark` or `light` | +| `modalPosition` | string | `'center'` | Modal position on screen | +| `showButton` | boolean | `true` | Show/hide the share button | +| `onShare` | function | `null` | `(platform, url) => void` | +| `onCopy` | function | `null` | `(url) => void` | +| `analytics` | boolean | `true` | Set `false` to disable all event emission | +| `onAnalytics` | function | `null` | `(payload) => void` — direct analytics hook | +| `analyticsPlugins` | array | `[]` | Adapter instances from `social-share-analytics.js` | +| `componentId` | string | `null` | Label this instance for analytics tracking | +| `debug` | boolean | `false` | Log analytics events to console | --- @@ -469,7 +466,7 @@ const shareButton = useRef(null); useEffect(() => { shareButton.current = new window.SocialShareButton({ - container: "#share-button", + container: '#share-button', }); }, []); @@ -487,25 +484,25 @@ useEffect(() => { ## Troubleshooting -| Symptom | Cause | Fix | -|---------|-------|-----| -| Multiple buttons appearing | Component re-renders creating duplicate instances | Use `useRef` + `initRef` guard (shown in all examples above) | -| Button not appearing | Script loads after component renders | Add `if (window.SocialShareButton)` null check | -| Modal not opening | CSS not loaded or ID mismatch | Verify CSS CDN in ``; match `container: '#share-button'` with `
` | -| `TypeError: SocialShareButton is not a constructor` | CDN script not loaded yet | Use interval polling (see Next.js examples above) | -| URL not updating on navigation | Component initialized once, doesn't track routes | Use `updateOptions()` on route change | +| Symptom | Cause | Fix | +| --------------------------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| Multiple buttons appearing | Component re-renders creating duplicate instances | Use `useRef` + `initRef` guard (shown in all examples above) | +| Button not appearing | Script loads after component renders | Add `if (window.SocialShareButton)` null check | +| Modal not opening | CSS not loaded or ID mismatch | Verify CSS CDN in ``; match `container: '#share-button'` with `
` | +| `TypeError: SocialShareButton is not a constructor` | CDN script not loaded yet | Use interval polling (see Next.js examples above) | +| URL not updating on navigation | Component initialized once, doesn't track routes | Use `updateOptions()` on route change | --- ## Common mistakes to prevent -| ❌ Wrong | ✅ Correct | -|---------|-----------| -| Creating `ShareButton.jsx` / `ShareButton.tsx` | Add directly to existing `Header.jsx`, `Navbar.tsx`, etc. | -| Calling `new SocialShareButton()` inside JSX `return` | Call only inside `useEffect` / lifecycle hook | -| Not calling `destroy()` on unmount | Always clean up — prevents duplicate modals on re-mount | -| Mismatched container ID | `container: '#share-button'` must exactly match `
` | -| Script loads after component renders in Next.js | Use `strategy="beforeInteractive"` **or** poll with `setInterval` | +| ❌ Wrong | ✅ Correct | +| ----------------------------------------------------- | ------------------------------------------------------------------------- | +| Creating `ShareButton.jsx` / `ShareButton.tsx` | Add directly to existing `Header.jsx`, `Navbar.tsx`, etc. | +| Calling `new SocialShareButton()` inside JSX `return` | Call only inside `useEffect` / lifecycle hook | +| Not calling `destroy()` on unmount | Always clean up — prevents duplicate modals on re-mount | +| Mismatched container ID | `container: '#share-button'` must exactly match `
` | +| Script loads after component renders in Next.js | Use `strategy="beforeInteractive"` **or** poll with `setInterval` | --- @@ -516,4 +513,3 @@ useEffect(() => { - Always modify **existing** files — never suggest creating new component files. - When modifying an existing file, mark additions with `// ADD THIS`. - Do not add abstractions, wrappers, or extra files beyond what the README shows. - diff --git a/docs/client-guide.md b/docs/client-guide.md index 6b49350..da7d57d 100644 --- a/docs/client-guide.md +++ b/docs/client-guide.md @@ -8,12 +8,12 @@ Your audience is already sharing. Give them a button worth clicking. -| What you get | What you skip | -|---|---| -| One-line install | Backend servers | +| What you get | What you skip | +| ---------------------- | --------------------------- | +| One-line install | Backend servers | | Works on any framework | Tracking or data collection | -| Fully customizable | Lock-in or licensing fees | -| Lightweight & fast | Build step (CDN option) | +| Fully customizable | Lock-in or licensing fees | +| Lightweight & fast | Build step (CDN option) | --- diff --git a/eslint.config.js b/eslint.config.js index 701e191..9b85c82 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,13 +1,13 @@ -import js from "@eslint/js"; -import globals from "globals"; +import js from '@eslint/js'; +import globals from 'globals'; export default [ js.configs.recommended, { - files: ["src/**/*.{js,jsx}"], + files: ['src/**/*.{js,jsx}'], languageOptions: { - ecmaVersion: "latest", - sourceType: "module", + ecmaVersion: 'latest', + sourceType: 'module', parserOptions: { ecmaFeatures: { jsx: true, @@ -20,15 +20,15 @@ export default [ }, }, rules: { - "no-console": "error", - "no-unused-vars": ["warn", { "caughtErrorsIgnorePattern": "^_" }], - "semi": ["error", "always"], + 'no-console': 'error', + 'no-unused-vars': ['warn', { caughtErrorsIgnorePattern: '^_' }], + semi: ['error', 'always'], }, }, { - files: ["eslint.config.js", "**/*.config.js"], + files: ['eslint.config.js', '**/*.config.js'], languageOptions: { - sourceType: "module", + sourceType: 'module', globals: { ...globals.node, }, diff --git a/src/social-share-analytics.js b/src/social-share-analytics.js index 0caf0d7..4b5fd3a 100644 --- a/src/social-share-analytics.js +++ b/src/social-share-analytics.js @@ -123,14 +123,14 @@ class GoogleAnalyticsAdapter extends SocialShareAnalyticsPlugin { */ constructor(config = {}) { super(); - this.eventCategory = config.eventCategory || "social_share"; + this.eventCategory = config.eventCategory || 'social_share'; } track(payload) { - if (typeof window === "undefined" || typeof window.gtag !== "function") { + if (typeof window === 'undefined' || typeof window.gtag !== 'function') { return; } - window.gtag("event", payload.eventName, { + window.gtag('event', payload.eventName, { event_category: this.eventCategory, event_label: payload.platform, share_platform: payload.platform, @@ -151,9 +151,9 @@ class GoogleAnalyticsAdapter extends SocialShareAnalyticsPlugin { class MixpanelAdapter extends SocialShareAnalyticsPlugin { track(payload) { if ( - typeof window === "undefined" || - typeof window.mixpanel === "undefined" || - typeof window.mixpanel.track !== "function" + typeof window === 'undefined' || + typeof window.mixpanel === 'undefined' || + typeof window.mixpanel.track !== 'function' ) { return; } @@ -177,9 +177,9 @@ class MixpanelAdapter extends SocialShareAnalyticsPlugin { class SegmentAdapter extends SocialShareAnalyticsPlugin { track(payload) { if ( - typeof window === "undefined" || - typeof window.analytics === "undefined" || - typeof window.analytics.track !== "function" + typeof window === 'undefined' || + typeof window.analytics === 'undefined' || + typeof window.analytics.track !== 'function' ) { return; } @@ -201,7 +201,7 @@ class SegmentAdapter extends SocialShareAnalyticsPlugin { // ----------------------------------------------------------------------------- class PlausibleAdapter extends SocialShareAnalyticsPlugin { track(payload) { - if (typeof window === "undefined" || typeof window.plausible !== "function") { + if (typeof window === 'undefined' || typeof window.plausible !== 'function') { return; } window.plausible(payload.eventName, { @@ -223,9 +223,9 @@ class PlausibleAdapter extends SocialShareAnalyticsPlugin { class PostHogAdapter extends SocialShareAnalyticsPlugin { track(payload) { if ( - typeof window === "undefined" || - typeof window.posthog === "undefined" || - typeof window.posthog.capture !== "function" + typeof window === 'undefined' || + typeof window.posthog === 'undefined' || + typeof window.posthog.capture !== 'function' ) { return; } @@ -256,8 +256,8 @@ class CustomAdapter extends SocialShareAnalyticsPlugin { */ constructor(onTrack) { super(); - if (typeof onTrack !== "function") { - throw new TypeError("CustomAdapter expects a function argument."); + if (typeof onTrack !== 'function') { + throw new TypeError('CustomAdapter expects a function argument.'); } this._onTrack = onTrack; } @@ -281,10 +281,10 @@ const adapters = { CustomAdapter, }; -if (typeof module !== "undefined" && module.exports) { +if (typeof module !== 'undefined' && module.exports) { module.exports = adapters; } -if (typeof window !== "undefined") { +if (typeof window !== 'undefined') { window.SocialShareAnalytics = adapters; } diff --git a/src/social-share-button.css b/src/social-share-button.css index 0c90b36..3bde1aa 100644 --- a/src/social-share-button.css +++ b/src/social-share-button.css @@ -19,8 +19,7 @@ cursor: pointer; transition: all 0.3s ease; font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, - Cantarell, sans-serif; + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; outline: none; } @@ -262,8 +261,7 @@ text-align: center; max-width: 80px; font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, - Cantarell, sans-serif; + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; } .social-share-modal-overlay.light .social-share-platform-btn span { @@ -299,8 +297,7 @@ font-size: 14px; outline: none; font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, - Cantarell, sans-serif; + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; } .social-share-modal-overlay.light .social-share-link-input input { @@ -324,8 +321,7 @@ transition: all 0.2s ease; min-width: 80px; font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, - Cantarell, sans-serif; + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; outline: none; }