diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee20f27..04b5fea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -146,6 +146,7 @@ Push your branch: ### Code Quality Tools We use ESLint for linting and Prettier for formatting. Please run these before submitting a PR: + - `npm run lint` — Check for code quality and style issues. - `npm run format` — Automatically format your code to project standards. - `npm run format:check` — Verify that files are correctly formatted. diff --git a/README.md b/README.md index 34933c9..8a4c0ec 100644 --- a/README.md +++ b/README.md @@ -66,14 +66,13 @@ Lightweight social sharing component for web applications. Zero dependencies, fr [![npm version](https://img.shields.io/npm/v/social-share-button-aossie.svg)](https://www.npmjs.com/package/social-share-button-aossie) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE) - --- ## Features - 🌐 Multiple platforms: WhatsApp, Facebook, X, LinkedIn, Telegram, Reddit, Email, Pinterest - 🎯 Zero dependencies - pure vanilla JavaScript -- ⚛️ Framework support: React, Preact, Next.js, Qwik, Vue, Angular, or plain HTML +- ⚛️ Framework support: React, Preact, Next.js, Nuxt.js, Qwik, Vue, Angular, WordPress or plain HTML - 🔄 Auto-detects current URL and page title - 📱 Fully responsive and mobile-ready - 🎨 Customizable themes (dark/light) @@ -104,11 +103,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` / `functions.php` | +| **2️⃣ Add Container** | Place `
` | The UI component or WordPress `wp_footer` hook | +| **3️⃣ Initialize** | Call `new SocialShareButton({ container: "#share-button" })` | Inside that component or WordPress `wp_footer` hook | > 💡 Pick your framework below for the full copy-paste snippet: @@ -438,12 +437,12 @@ new window.SocialShareButton({
- + ``` @@ -487,6 +486,95 @@ export default function Header() { +
+🔷 WordPress + +### Step 1: Enqueue in `functions.php` + +Add the following to your theme's `functions.php` to load the library directly from this repository via jsDelivr CDN: + +> **Note:** This package is not published to npm. Use the jsDelivr + GitHub CDN link below to load the correct distributable from the [AOSSIE-Org/SocialShareButton](https://github.com/AOSSIE-Org/SocialShareButton) repository. + +```php +function enqueue_social_share_button() { + wp_enqueue_style( + 'social-share-button', + 'https://cdn.jsdelivr.net/gh/AOSSIE-Org/SocialShareButton@v1.0.3/src/social-share-button.css' + ); + wp_enqueue_script( + 'social-share-button', + 'https://cdn.jsdelivr.net/gh/AOSSIE-Org/SocialShareButton@v1.0.3/src/social-share-button.js', + [], + null, + true // Load in footer + ); +} +add_action('wp_enqueue_scripts', 'enqueue_social_share_button'); +``` + +### Step 2: Initialize in `functions.php` (Footer Hook) + +Use the `wp_footer` hook with a **priority of 21** to inject the container and initialization script. The priority must be higher than the default (10) so WordPress prints the enqueued footer scripts _before_ this function runs: + +```php +function init_social_share_button() { ?> +
+ + + +
+💚 Nuxt.js + +### Step 1: Add CDN to your layout file (e.g., `app.vue` or `layouts/default.vue`) + +```html + + + + + + +``` + +### Step 2: Obtain the Nuxt wrapper component +Currently, the wrapper is not available via CDN and must be added manually. Copy the `src/social-share-button-nuxt.vue` file from this repository into your Nuxt project's `components/` folder. Rename it to `SocialShareButton.vue` to match the usage below. + +### Step 3: Use the component in your page or component +Open an **existing** page — typically `pages/index.vue`. Since the component is in the `components/` folder, Nuxt 3 will auto-import it. + +```vue + + + +``` + +
+ --- ## Configuration diff --git a/docs/Roadmap.md b/docs/Roadmap.md index 98ae879..bee023d 100644 --- a/docs/Roadmap.md +++ b/docs/Roadmap.md @@ -1,7 +1,7 @@ # 🚀 SocialShareButton — Project Roadmap > **Version:** 2.0 Draft -> **Status:** Living Document +> **Status:** Living Document --- @@ -27,23 +27,23 @@ This section is the ground truth before any planning. ### ✅ What Already Exists -| Feature | Status | Notes | -|---|---|---| -| CDN distribution (jsDelivr) | ✅ | `v1.0.3` | -| npm package | ✅ | Published as `social-share-button-aossie` (unscoped) | -| 7 share platforms | ✅ | WhatsApp, Facebook, X, LinkedIn, Telegram, Reddit, Email | -| `onShare` callback | ✅ | `(platform, url) => {}` | -| `onCopy` callback | ✅ | `(url) => {}` | -| `theme: 'dark' \| 'light'` | ✅ | Basic two-mode theming | -| `buttonColor` / `buttonHoverColor` | ✅ | Programmatic color overrides | -| `customClass` | ✅ | Escape hatch for custom CSS | -| `modalPosition` | ✅ | Modal placement config | -| `updateOptions()` | ✅ | SPA dynamic URL updates | -| React wrapper | ✅ | Exists as `src/social-share-button-react.jsx` (copy-paste only) | -| TypeScript types | ❌ | None shipped | -| Scoped npm package | ❌ | Not yet (`@social-share/core` etc.) | -| Framework packages | ❌ | No installable Vue / Qwik / Solid packages | -| Proper CSS build artifact | ❌ | CSS imported from `src/` path — breaks in most bundlers | +| Feature | Status | Notes | +| ---------------------------------- | ------ | --------------------------------------------------------------- | +| CDN distribution (jsDelivr) | ✅ | `v1.0.3` | +| npm package | ✅ | Published as `social-share-button-aossie` (unscoped) | +| 7 share platforms | ✅ | WhatsApp, Facebook, X, LinkedIn, Telegram, Reddit, Email | +| `onShare` callback | ✅ | `(platform, url) => {}` | +| `onCopy` callback | ✅ | `(url) => {}` | +| `theme: 'dark' \| 'light'` | ✅ | Basic two-mode theming | +| `buttonColor` / `buttonHoverColor` | ✅ | Programmatic color overrides | +| `customClass` | ✅ | Escape hatch for custom CSS | +| `modalPosition` | ✅ | Modal placement config | +| `updateOptions()` | ✅ | SPA dynamic URL updates | +| React wrapper | ✅ | Exists as `src/social-share-button-react.jsx` (copy-paste only) | +| TypeScript types | ❌ | None shipped | +| Scoped npm package | ❌ | Not yet (`@social-share/core` etc.) | +| Framework packages | ❌ | No installable Vue / Qwik / Solid packages | +| Proper CSS build artifact | ❌ | CSS imported from `src/` path — breaks in most bundlers | ### ⚠️ Known Issues to Fix Before Any New Features @@ -108,13 +108,13 @@ social-share-button/ ← Turborepo monorepo root ### Layer Responsibilities -| Layer | Package | Responsibility | -|---|---|---| -| Core Engine | `@social-share/core` | Platform logic, URL building, config validation | -| Analytics | `@social-share/analytics` | Event emission, consent, adapter routing | -| Theme | `@social-share/theme` | CSS tokens, presets, Theme Designer, export | -| Framework Wrappers | `@social-share/react` etc. | Framework-specific components using core | -| CDN Build | `apps/cdn-build` | Bundles core + wrappers into single distributable | +| Layer | Package | Responsibility | +| ------------------ | -------------------------- | ------------------------------------------------- | +| Core Engine | `@social-share/core` | Platform logic, URL building, config validation | +| Analytics | `@social-share/analytics` | Event emission, consent, adapter routing | +| Theme | `@social-share/theme` | CSS tokens, presets, Theme Designer, export | +| Framework Wrappers | `@social-share/react` etc. | Framework-specific components using core | +| CDN Build | `apps/cdn-build` | Bundles core + wrappers into single distributable | --- @@ -122,7 +122,7 @@ social-share-button/ ← Turborepo monorepo root --- -### ✅ Phase 0 — Stabilization *(Now — before any refactoring)* +### ✅ Phase 0 — Stabilization _(Now — before any refactoring)_ **Goal:** Fix the broken npm package experience and document the full API surface. No new features. @@ -181,6 +181,7 @@ export function executeShare(platform: Platform, config: ShareConfig): void { .. **Goal:** Replace the copy-paste React wrapper with installable framework packages. React first since it already exists as `.jsx`. **Priority order:** + 1. `@social-share/react` — replaces `src/social-share-button-react.jsx` 2. `@social-share/vue` — Composition API component 3. `@social-share/qwik` — Resumable, SSR-safe (open issue) @@ -191,26 +192,27 @@ export function executeShare(platform: Platform, config: ShareConfig): void { .. ```typescript interface SocialShareButtonProps { - url?: string; // default: window.location.href - title?: string; // default: document.title + url?: string; // default: window.location.href + title?: string; // default: document.title description?: string; hashtags?: string[]; via?: string; platforms?: Platform[]; buttonText?: string; - buttonStyle?: 'default' | 'primary' | 'compact' | 'icon-only'; - buttonColor?: string; // existing API - buttonHoverColor?: string; // existing API - customClass?: string; // existing API - theme?: 'dark' | 'light' | ThemeTokens; // string shorthand still works - analytics?: AnalyticsConfig; // Phase 3 + buttonStyle?: "default" | "primary" | "compact" | "icon-only"; + buttonColor?: string; // existing API + buttonHoverColor?: string; // existing API + customClass?: string; // existing API + theme?: "dark" | "light" | ThemeTokens; // string shorthand still works + analytics?: AnalyticsConfig; // Phase 3 componentId?: string; - onShare?: (platform: Platform, url: string) => void; // same signature as today - onCopy?: (url: string) => void; // same signature as today + onShare?: (platform: Platform, url: string) => void; // same signature as today + onCopy?: (url: string) => void; // same signature as today } ``` **Migration for existing React wrapper users:** + ```tsx // Before (copy-paste) import { SocialShareButton } from "./components/SocialShareButton"; @@ -226,7 +228,7 @@ import { SocialShareButton } from "@social-share/react"; --- -### 🔬 Phase 3 — Analytics Module +### 🔬 Phase 3 — Analytics Module **Goal:** Ship `@social-share/analytics` — a privacy-first, pluggable analytics layer that uses the existing `onShare` / `onCopy` callbacks as its internal trigger mechanism. @@ -234,25 +236,25 @@ import { SocialShareButton } from "@social-share/react"; ```typescript // Path 1: DOM Events — zero config, works with any analytics tool -document.addEventListener('ssb:share', (e) => console.log(e.detail)); -document.addEventListener('ssb:copy', (e) => console.log(e.detail)); -document.addEventListener('ssb:modal_open', (e) => console.log(e.detail)); -document.addEventListener('ssb:modal_close', (e) => console.log(e.detail)); +document.addEventListener("ssb:share", (e) => console.log(e.detail)); +document.addEventListener("ssb:copy", (e) => console.log(e.detail)); +document.addEventListener("ssb:modal_open", (e) => console.log(e.detail)); +document.addEventListener("ssb:modal_close", (e) => console.log(e.detail)); // Path 2: Single callback hook — simplest npm integration new SocialShareButton({ analytics: { - onEvent: (event: SSBEvent) => myAnalytics.track(event.name, event.data) - } + onEvent: (event: SSBEvent) => myAnalytics.track(event.name, event.data), + }, }); // Path 3: Named adapter — built-in wiring for popular tools -import { GA4Adapter } from '@social-share/analytics/adapters/ga4'; +import { GA4Adapter } from "@social-share/analytics/adapters/ga4"; new SocialShareButton({ analytics: { - adapter: new GA4Adapter({ measurementId: 'G-XXXXXXXX' }) - } + adapter: new GA4Adapter({ measurementId: "G-XXXXXXXX" }), + }, }); ``` @@ -260,31 +262,31 @@ new SocialShareButton({ ```typescript interface SSBEvent { - name: 'ssb:share' | 'ssb:copy' | 'ssb:modal_open' | 'ssb:modal_close'; + name: "ssb:share" | "ssb:copy" | "ssb:modal_open" | "ssb:modal_close"; platform?: Platform; - componentId?: string; // developer-defined identifier + componentId?: string; // developer-defined identifier url: string; timestamp: number; - sessionId: string; // anonymous, ephemeral, never persisted + sessionId: string; // anonymous, ephemeral, never persisted } ``` #### Built-in Adapters -| Adapter | Notes | -|---|---| -| `GA4Adapter` | `gtag('event', ...)` — no PII sent | -| `MixpanelAdapter` | `mixpanel.track()` — requires Mixpanel loaded | -| `SegmentAdapter` | `analytics.track()` — wraps Segment's standard API | -| `PlausibleAdapter` | `plausible()` custom event — cookieless by default | -| `PostHogAdapter` | `posthog.capture()` — EU cloud + self-hosted compatible | -| `CustomAdapter` | Extend `BaseAdapter`, implement `track(event: SSBEvent): void` | +| Adapter | Notes | +| ------------------ | -------------------------------------------------------------- | +| `GA4Adapter` | `gtag('event', ...)` — no PII sent | +| `MixpanelAdapter` | `mixpanel.track()` — requires Mixpanel loaded | +| `SegmentAdapter` | `analytics.track()` — wraps Segment's standard API | +| `PlausibleAdapter` | `plausible()` custom event — cookieless by default | +| `PostHogAdapter` | `posthog.capture()` — EU cloud + self-hosted compatible | +| `CustomAdapter` | Extend `BaseAdapter`, implement `track(event: SSBEvent): void` | #### Privacy Controls ```typescript -SocialShareButton.analytics.enable(); // call after consent granted -SocialShareButton.analytics.disable(); // opt-out / consent withdrawn +SocialShareButton.analytics.enable(); // call after consent granted +SocialShareButton.analytics.disable(); // opt-out / consent withdrawn SocialShareButton.analytics.isEnabled(); // boolean SocialShareButton.analytics.debug(true); // log to console, don't send ``` @@ -298,7 +300,7 @@ SocialShareButton.analytics.debug(true); // log to console, don't send --- -### 🎨 Phase 4 — Theme System + Theme Designer +### 🎨 Phase 4 — Theme System + Theme Designer **Goal:** Extend existing `dark` / `light` theming into a full CSS-variable-based system with an interactive Theme Designer. All existing `theme`, `buttonColor`, `buttonHoverColor`, `customClass` options remain fully supported. @@ -311,7 +313,7 @@ SocialShareButton.analytics.debug(true); // log to console, don't send --ssb-btn-bg: #1da1f2; --ssb-btn-bg-hover: #0d8fd9; --ssb-btn-radius: 6px; - --ssb-btn-shadow: 0 2px 8px rgba(0,0,0,0.12); + --ssb-btn-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); /* Per-platform icon colors */ --ssb-icon-twitter: #1da1f2; @@ -327,7 +329,7 @@ SocialShareButton.analytics.debug(true); // log to console, don't send --ssb-modal-animation-speed: 200ms; --ssb-font-family: system-ui, sans-serif; - --ssb-shape: rounded; /* rounded | pill | square */ + --ssb-shape: rounded; /* rounded | pill | square */ } ``` @@ -336,6 +338,7 @@ SocialShareButton.analytics.debug(true); // log to console, don't send Hosted at `apps/playground`. **Controls:** + - Colors & Gradients — solid + gradient builder per button - Shapes — rounded / pill / square - Border — width, color, style @@ -344,6 +347,7 @@ Hosted at `apps/playground`. - Font family, shadow intensity, hover effect selector **Export formats:** + - CSS variables block - `.json` theme file (SDK-importable) - Shareable URL (theme as URL params) @@ -369,38 +373,41 @@ import myTheme from './my-theme.json'; // from Theme Designer **Goal:** CDN and SDK reach full feature parity. Complete all planned server-side and CMS integrations. **CDN feature parity:** + - Bundle includes analytics + theming (opt-in at build config level) - Config via `data-ssb-*` HTML attributes or `window.SocialShareButtonConfig` - SRI hash generation in CI, hosted on jsDelivr + unpkg **Platform integrations:** -| Integration | Delivery | Issue Status | -|---|---|---| -| Remix | `@social-share/react` + SSR guide | 🟡 Open | -| Solid.js | `@social-share/solid` | 🟡 Open | -| Rails | Gem wrapper + CDN tag helper | 🟡 Open | -| Django | Template tag + CDN | 🟡 Open | -| Laravel | Blade component + CDN | 🟡 Open | -| WordPress | Plugin (CDN-backed) | 🟡 Open | -| Hugo | Shortcode + CDN | 🟡 Open | -| Jekyll | Include template + CDN | 🟡 Open | -| Web Components (Lit) | `@social-share/wc` | 🟡 Open | -| Alpine.js | `x-data` binding guide + CDN | 🟡 Open | +| Integration | Delivery | Issue Status | +| -------------------- | --------------------------------- | ------------ | +| Remix | `@social-share/react` + SSR guide | 🟡 Open | +| Solid.js | `@social-share/solid` | 🟡 Open | +| Rails | Gem wrapper + CDN tag helper | 🟡 Open | +| Django | Template tag + CDN | 🟡 Open | +| Laravel | Blade component + CDN | 🟡 Open | +| WordPress | Plugin (CDN-backed) | 🟡 Open | +| Hugo | Shortcode + CDN | 🟡 Open | +| Jekyll | Include template + CDN | 🟡 Open | +| Web Components (Lit) | `@social-share/wc` | 🟡 Open | +| Alpine.js | `x-data` binding guide + CDN | 🟡 Open | CI smoke test per integration: spin up minimal app, assert button renders and emits share event. --- -### ⚡ Phase 6 — Advanced Features & Ecosystem +### ⚡ Phase 6 — Advanced Features & Ecosystem **Accessibility:** + - Full ARIA — `role="button"`, `aria-label`, `aria-expanded` on modal - Keyboard navigation — Tab, Enter, Escape - `prefers-reduced-motion` support (maps to `--ssb-modal-animation-speed: 0ms`) - WCAG 2.1 AA tested with axe-core in CI **Performance:** + - CDN bundle: target < 8KB gzipped - npm packages: tree-shakeable — twitter-only import ~1KB - CSS: single `@layer` block, no `@import` chains @@ -409,22 +416,24 @@ CI smoke test per integration: spin up minimal app, assert button renders and em ```typescript SocialShareButton.registerPlatform({ - id: 'bluesky', - label: 'Bluesky', + id: "bluesky", + label: "Bluesky", icon: BlueskyIcon, buildURL: (config) => `https://bsky.app/intent/compose?text=${config.title} ${config.url}`, }); ``` **Native Web Share API:** + ```typescript new SocialShareButton({ - preferNativeShare: 'mobile-only', // true | false | 'mobile-only' + preferNativeShare: "mobile-only", // true | false | 'mobile-only' }); // Falls back to custom modal when navigator.share() is unavailable ``` **Monorepo tooling maturity:** + - Changesets-based automated releases via GitHub Actions - Per-package changelogs - Canary / beta release channel @@ -434,20 +443,20 @@ new SocialShareButton({ ## 🤝 Contribution Opportunities -| Area | Skills Needed | Phase | -|---|---|---| -| Fix CSS export path + `exports` field | npm packaging | 0 | -| Write `.d.ts` TypeScript declarations | TypeScript | 0 | -| Core engine extraction | TypeScript, DOM APIs | 1 | -| Turborepo + pnpm workspace setup | Monorepo tooling | 1 | -| `@social-share/react` (from existing jsx) | React | 2 | -| `@social-share/vue` / `solid` / `qwik` | Vue / Solid / Qwik | 2 | -| Analytics adapters | GA4 / PostHog / Segment APIs | 3 | -| Theme Designer UI | React, CSS variables | 4 | -| CMS / server-side integrations | Rails / Django / Laravel / WP | 5 | -| Accessibility audit | WCAG, axe-core | 6 | -| Docs site | Next.js, MDX | Ongoing | -| CI/CD pipelines | GitHub Actions, Changesets | Ongoing | +| Area | Skills Needed | Phase | +| ----------------------------------------- | ----------------------------- | ------- | +| Fix CSS export path + `exports` field | npm packaging | 0 | +| Write `.d.ts` TypeScript declarations | TypeScript | 0 | +| Core engine extraction | TypeScript, DOM APIs | 1 | +| Turborepo + pnpm workspace setup | Monorepo tooling | 1 | +| `@social-share/react` (from existing jsx) | React | 2 | +| `@social-share/vue` / `solid` / `qwik` | Vue / Solid / Qwik | 2 | +| Analytics adapters | GA4 / PostHog / Segment APIs | 3 | +| Theme Designer UI | React, CSS variables | 4 | +| CMS / server-side integrations | Rails / Django / Laravel / WP | 5 | +| Accessibility audit | WCAG, axe-core | 6 | +| Docs site | Next.js, MDX | Ongoing | +| CI/CD pipelines | GitHub Actions, Changesets | Ongoing | > 💡 Phase 0 tasks are labeled `good-first-issue` and require no monorepo knowledge — ideal starting point for new contributors. @@ -465,16 +474,16 @@ new SocialShareButton({ ## 📊 Distribution Strategy -| Path | Audience | Package | Status | -|---|---|---|---| -| CDN (` diff --git a/src/social-share-button-preact.jsx b/src/social-share-button-preact.jsx index 012e66f..3d9b7cf 100644 --- a/src/social-share-button-preact.jsx +++ b/src/social-share-button-preact.jsx @@ -1,6 +1,6 @@ import { useEffect, useRef } from "preact/hooks"; - /** +/** * SocialShareButton Preact Wrapper * * Provides a lightweight Preact functional component that wraps the core @@ -26,7 +26,7 @@ export default function SocialShareButton({ onCopy = null, buttonStyle = "default", modalPosition = "center", - + // Analytics props — the library emits events but never collects data itself. analytics = true, onAnalytics = null, // (payload) => void hook @@ -36,10 +36,10 @@ export default function SocialShareButton({ }) { // DOM reference to the container where the button will be injected const containerRef = useRef(null); - + // Reference to the vanilla JS class instance const shareButtonRef = useRef(null); - + // Storage for the latest options to avoid stale closures during async initialization const latestOptionsRef = useRef(null); @@ -74,9 +74,9 @@ export default function SocialShareButton({ /** * Initialization Effect - * + * * Handles the setup of the vanilla JS instance once the component mounts. - * Includes a polling mechanism to wait for the core library if it's loaded + * Includes a polling mechanism to wait for the core library if it's loaded * asynchronously (e.g., via a CDN script tag). */ useEffect(() => { @@ -131,12 +131,12 @@ export default function SocialShareButton({ /** * Update Effect - * - * Synchronizes prop changes from Preact down to the vanilla JS instance + * + * Synchronizes prop changes from Preact down to the vanilla JS instance * without re-mounting the entire component. */ - - // Stringify array dependencies to prevent unnecessary re-runs when + + // Stringify array dependencies to prevent unnecessary re-runs when // parent components pass fresh array literals on every render. const hashtagsDep = JSON.stringify(hashtags); const platformsDep = JSON.stringify(platforms); diff --git a/src/social-share-button-react.jsx b/src/social-share-button-react.jsx index 24eb3b2..45ac8f8 100644 --- a/src/social-share-button-react.jsx +++ b/src/social-share-button-react.jsx @@ -3,8 +3,8 @@ import { useEffect, useRef } from "react"; /** * SocialShareButton React Wrapper * - * Provides a React functional component that wraps the core SocialShareButton - * vanilla JS library. Handles lifecycle, dynamic updates, and provides + * Provides a React functional component that wraps the core SocialShareButton + * vanilla JS library. Handles lifecycle, dynamic updates, and provides * sensible defaults for all sharing options. */ export const SocialShareButton = ({ @@ -21,7 +21,7 @@ export const SocialShareButton = ({ onCopy = null, buttonStyle = "default", modalPosition = "center", - + // Analytics: library emits events but never collects data analytics = true, onAnalytics = null, // Event callback @@ -31,7 +31,7 @@ export const SocialShareButton = ({ }) => { // DOM reference for the injection target const containerRef = useRef(null); - + // Reference to the vanilla JS class instance const shareButtonRef = useRef(null); @@ -41,52 +41,52 @@ export const SocialShareButton = ({ /** * Initialization Effect - * + * * Sets up the vanilla JS component once the React component mounts. * Includes a safe check for the global SocialShareButton class. */ useEffect(() => { - if (containerRef.current && !shareButtonRef.current) { - if (typeof window !== "undefined" && window.SocialShareButton) { - shareButtonRef.current = new window.SocialShareButton({ - container: containerRef.current, - url: currentUrl, - title: currentTitle, - description, - hashtags, - via, - platforms, - theme, - buttonText, - customClass, - onShare, - onCopy, - buttonStyle, - modalPosition, - analytics, - onAnalytics, - analyticsPlugins, - componentId, - debug, - }); - } + if (containerRef.current && !shareButtonRef.current) { + if (typeof window !== "undefined" && window.SocialShareButton) { + shareButtonRef.current = new window.SocialShareButton({ + container: containerRef.current, + url: currentUrl, + title: currentTitle, + description, + hashtags, + via, + platforms, + theme, + buttonText, + customClass, + onShare, + onCopy, + buttonStyle, + modalPosition, + analytics, + onAnalytics, + analyticsPlugins, + componentId, + debug, + }); } + } - return () => { - if (shareButtonRef.current) { - shareButtonRef.current.destroy(); - shareButtonRef.current = null; - } - }; - }, []); + return () => { + if (shareButtonRef.current) { + shareButtonRef.current.destroy(); + shareButtonRef.current = null; + } + }; + }, []); /** * Update Effect - * - * Synchronizes React prop changes with the underlying vanilla JS instance + * + * Synchronizes React prop changes with the underlying vanilla JS instance * without re-mounting the entire component. */ - + useEffect(() => { if (shareButtonRef.current) { // Use the library's built-in update method diff --git a/src/social-share-button.css b/src/social-share-button.css index d42c10b..0609839 100644 --- a/src/social-share-button.css +++ b/src/social-share-button.css @@ -238,7 +238,6 @@ background: rgba(255, 255, 255, 0.4); } - /* Individual Platform Button */ .social-share-platform-btn { display: flex; @@ -255,8 +254,8 @@ } .social-share-platform-btn:hover { - transform: scale(1.05); - } + transform: scale(1.05); +} .social-share-platform-btn:active { transform: scale(0.95); @@ -278,10 +277,10 @@ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); } -.social-share-platform-icon svg { +.social-share-platform-icon svg { width: 28px; - height: 28px; - } + height: 28px; +} .social-share-platform-btn span { font-size: 12px; @@ -375,7 +374,6 @@ background: #00b857; } - /* ----------------------------------------------------------------------------- RESPONSIVE OVERRIDES ----------------------------------------------------------------------------- */ @@ -389,11 +387,10 @@ } /* Snap modal to bottom on mobile */ - .social-share-modal-content.bottom { - margin-top: auto; + .social-share-modal-content.bottom { + margin-top: auto; } - .social-share-modal-header { padding: 12px 16px; } @@ -450,7 +447,6 @@ color: #fff; } - /* ----------------------------------------------------------------------------- ACCESSIBILITY & PRINT ----------------------------------------------------------------------------- */ diff --git a/src/social-share-button.js b/src/social-share-button.js index 3905128..7b5d203 100644 --- a/src/social-share-button.js +++ b/src/social-share-button.js @@ -85,15 +85,10 @@ class SocialShareButton { `; this.button = button; - if (this.options.container) { - const container = - typeof this.options.container === "string" - ? document.querySelector(this.options.container) - : this.options.container; + const container = this._getContainer(); - if (container) { - container.appendChild(button); - } + if (container) { + container.appendChild(button); } } @@ -684,9 +679,26 @@ class SocialShareButton { _getContainer() { if (!this.options.container) return null; if (typeof document === "undefined") return null; - return typeof this.options.container === "string" - ? document.querySelector(this.options.container) - : this.options.container; + + let container = null; + try { + container = + typeof this.options.container === "string" + ? document.querySelector(this.options.container) + : this.options.container; + } catch (error) { + this._debugWarn("Invalid container selector provided in _getContainer", error); + return null; + } + + // Safety check: ensure the resolved value is actually a DOM Element. + // This prevents crashes if a user passes a non-DOM object to the container option. + if (container && !(container instanceof Element || container.nodeType === 1)) { + this._debugWarn(`Provided container is not a valid DOM Element: ${container}`, null); + return null; + } + + return container; } /** @@ -799,4 +811,4 @@ if (typeof module !== "undefined" && module.exports) { if (typeof window !== "undefined") { window.SocialShareButton = SocialShareButton; -} \ No newline at end of file +}