UNPKG

@vitalwall/client

Version:

VitalWall client library for vanilla JavaScript applications

995 lines (975 loc) 42.9 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.VitalWall = {})); })(this, (function (exports) { 'use strict'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; // Browser environment detection const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'; class VitalWall { constructor() { this.apiKey = ''; this.baseUrl = ''; this.domain = ''; this.projectId = ''; this.debug = false; this.initialized = false; this.initializationPromise = null; this.currentWallId = null; this.accessToken = null; this.refreshToken = null; this.tokenExpiryTime = null; this.subscription = null; this.initializedWalls = new Set(); this.mutationObserver = null; this.readyCallbacks = []; // Track current items and config per wall for incremental updates this.wallData = new Map(); } init(config) { return __awaiter(this, void 0, void 0, function* () { if (!config.apiKey) { this.error('VitalWall: apiKey is required'); throw new Error('VitalWall: apiKey is required'); } this.apiKey = config.apiKey; this.baseUrl = 'https://clyfgobyllqtvmfchczj.supabase.co/functions/v1'; // Auto-detect domain in browser, require explicit domain in Node.js for security if (isBrowser) { this.domain = window.location.hostname; } else { if (!config.domain) { this.error('VitalWall: domain is required when running in Node.js environment'); throw new Error('VitalWall: domain is required when running in Node.js environment'); } this.domain = config.domain; } this.projectId = this.baseUrl.split('/')[2].split('.')[0]; this.debug = config.debug || false; this.currentWallId = config.wallId || null; this.log('VitalWall initialized with domain:', this.domain); // Try to load existing token (browser only) if (isBrowser) { const localStorage = window.localStorage; const storedToken = localStorage.getItem('vitalwall-token'); if (storedToken) { try { const tokenData = JSON.parse(storedToken); if (tokenData.domain === this.domain && Date.now() < tokenData.expiryTime) { this.log('Using stored token'); this.accessToken = tokenData.accessToken; this.refreshToken = tokenData.refreshToken; this.tokenExpiryTime = tokenData.expiryTime; if (!this.currentWallId && tokenData.wallId) { this.currentWallId = tokenData.wallId; } this.scheduleTokenRefresh(); this.initialized = true; this.setupWallObserver(); this.setupTracking(); this.dispatchReadyEvent(); this.checkForWalls(); return; } } catch (e) { this.error('Error parsing stored token:', e); } } } this.accessToken = null; this.refreshToken = null; this.tokenExpiryTime = null; // Initialize tokens and setup this.initializationPromise = this.initializeTokens().then(() => { if (isBrowser) { this.setupWallObserver(); this.setupTracking(); this.checkForWalls(); } this.initialized = true; this.dispatchReadyEvent(); }); return this.initializationPromise; }); } addItem(item) { return __awaiter(this, void 0, void 0, function* () { const wallId = item.wallId || this.currentWallId; if (!wallId) { this.error('No wall ID specified and no current wall set'); return undefined; } return new Promise((resolve, reject) => { this.sendUpdate(item.content, item.isHtml || false, wallId, (itemId) => { resolve(itemId); }, (error) => { reject(error); }); }); }); } addTextItem(content, wallId) { return __awaiter(this, void 0, void 0, function* () { return this.addItem({ content, isHtml: false, wallId }); }); } addHtmlItem(content, wallId) { return __awaiter(this, void 0, void 0, function* () { return this.addItem({ content, isHtml: true, wallId }); }); } setCurrentWall(wallId) { this.currentWallId = wallId; // Update stored token with new wall ID (browser only) if (isBrowser) { const storedToken = localStorage.getItem('vitalwall-token'); if (storedToken) { try { const tokenData = JSON.parse(storedToken); tokenData.wallId = wallId; localStorage.setItem('vitalwall-token', JSON.stringify(tokenData)); } catch (e) { this.error('Error updating stored token with wall ID:', e); } } } } getCurrentWall() { return this.currentWallId; } isInitialized() { return this.initialized; } onReady(callback) { if (this.initialized) { callback(); } else { this.readyCallbacks.push(callback); } } cleanup() { this.cleanupAllWalls(); if (this.mutationObserver) { this.mutationObserver.disconnect(); this.mutationObserver = null; } if (this.subscription) { this.subscription.unsubscribe(); this.subscription = null; } this.initialized = false; this.initializationPromise = null; } log(...args) { if (this.debug) { console.log('[VitalWall]', ...args); } } error(...args) { if (this.debug) { console.error('[VitalWall]', ...args); } } dispatchReadyEvent() { if (isBrowser) { window.dispatchEvent(new Event('vitalwall-loaded')); } this.readyCallbacks.forEach(callback => callback()); this.readyCallbacks = []; } checkForWalls() { if (!isBrowser) return; const maxAttempts = 20; // 10 seconds total let attempts = 0; const check = () => { if (attempts >= maxAttempts) return; attempts++; const walls = document.querySelectorAll('[data-vital-wall]:not([data-vital-wall-initialized])'); if (walls.length > 0) { this.initializeWalls(); return; } // Schedule next check setTimeout(check, 500); }; // Start checking check(); } initializeWalls() { if (!isBrowser) return; const wallElements = document.querySelectorAll('[data-vital-wall]:not([data-vital-wall-initialized])'); wallElements.forEach(element => this.initializeWall(element)); } setupWallObserver() { if (!isBrowser || !document.body) return; this.mutationObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { const element = node; if (element.hasAttribute('data-vital-wall')) { this.initializeWall(element); } // Check for child elements with data-vital-wall const children = element.querySelectorAll('[data-vital-wall]'); children.forEach(child => this.initializeWall(child)); } }); }); }); this.mutationObserver.observe(document.body, { childList: true, subtree: true }); } initializeWall(element) { if (!isBrowser || !element || element.hasAttribute('data-vital-wall-initialized')) { return; } // Get wall ID from data attribute const wallId = element.getAttribute('data-vital-wall'); if (!wallId) { this.error('data-vital-wall attribute must contain a wall ID'); return; } if (!this.currentWallId) { this.currentWallId = wallId; } // Store the wall ID with the token data (browser only) if (isBrowser) { const localStorage = window.localStorage; const storedToken = localStorage.getItem('vitalwall-token'); if (storedToken) { try { const tokenData = JSON.parse(storedToken); if (!tokenData.wallId) { tokenData.wallId = wallId; localStorage.setItem('vitalwall-token', JSON.stringify(tokenData)); } } catch (e) { this.error('Error updating stored token:', e); } } } // Style the element and create container this.styleWallElement(element); // Create container const container = document.createElement('div'); container.className = 'vital-wall-container'; container.style.cssText = ` width: 100%; max-width: 100%; box-sizing: border-box; position: relative; overflow: auto; `; // Add loading state container.innerHTML = '<div class="wall-item">Loading...</div>'; // Clear element and add container element.innerHTML = ''; element.appendChild(container); // Add base styles this.addBaseStyles(); // Store cleanup function element._cleanup = () => { if (this.subscription) { this.subscription.unsubscribe(); this.subscription = null; } }; // Add mutation observer to detect when element is removed if (isBrowser && element.parentNode) { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.removedNodes.forEach((node) => { if (node === element && element._cleanup) { element._cleanup(); } }); }); }); observer.observe(element.parentNode, { childList: true }); } // Load items and setup WebSocket this.loadItems(wallId, container); this.setupWebSocket(wallId, container); // Mark as initialized element.setAttribute('data-vital-wall-initialized', 'true'); this.initializedWalls.add(element); } styleWallElement(element) { element.style.cssText = ` display: block; width: 100%; max-width: 100%; overflow: auto; box-sizing: border-box; position: relative; `; } addBaseStyles() { if (!isBrowser) return; // Check if styles already added if (document.getElementById('vitalwall-styles')) return; const style = document.createElement('style'); style.id = 'vitalwall-styles'; style.textContent = ` .vital-wall-container { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; position: relative; } .wall-item { background: white; border-radius: 4px; padding: 10px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); box-sizing: border-box; } /* List layout specific styles */ .vital-wall-container.list-layout { display: flex; flex-direction: column; gap: 1rem; overflow-y: auto; height: 100%; } /* Grid layout specific styles */ .vital-wall-container.grid-layout { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1rem; padding: 1rem; } .vital-wall-container.grid-layout .wall-item { height: 100%; } /* Ticker-specific styles */ .ticker-viewport { position: relative; width: 100%; overflow: hidden; } .ticker-scroll { display: flex; flex-direction: row-reverse; gap: 1rem; padding: 1rem; overflow-x: auto; } .ticker-scroll .wall-item { flex: 0 0 auto; min-width: 200px; max-width: 300px; } .vital-status-container { position: relative; display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #666; padding: 8px; margin-top: 8px; } `; document.head.appendChild(style); } applyCustomCss(css, wallId) { if (!isBrowser || !css) return; const styleId = `vitalwall-custom-${wallId}`; let styleElement = document.getElementById(styleId); if (!styleElement) { styleElement = document.createElement('style'); styleElement.id = styleId; document.head.appendChild(styleElement); } // Scope CSS to this specific wall const scopedCss = css.replace(/([^{}]+)\{/g, (match, selector) => { const selectors = selector.split(',').map((s) => s.trim()); const scopedSelectors = selectors.map((s) => { if (s.startsWith('@') || s === ':root' || s.includes(`[data-vital-wall="${wallId}"]`)) { return s; } return `[data-vital-wall="${wallId}"] ${s}`; }); return `${scopedSelectors.join(', ')}{`; }); styleElement.textContent = scopedCss; } loadItems(wallId, container) { return __awaiter(this, void 0, void 0, function* () { if (!this.accessToken) { this.error('No access token available'); return; } try { const response = yield fetch(`${this.baseUrl}/wall-items?wall_id=${wallId}`, { headers: { 'Authorization': `Bearer ${this.accessToken}`, 'Origin': isBrowser ? window.location.origin : this.domain } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = yield response.json(); if (data.error) throw new Error(data.error); this.renderItems(data, container, wallId); } catch (error) { this.error('Error loading items:', error); container.innerHTML = '<div class="wall-item">Error loading wall data</div>'; } }); } setupWebSocket(wallId, container) { if (!isBrowser) return; // Use the anon key for initial connection, but set up authenticated session const anonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNseWZnb2J5bGxxdHZtZmNoY3pqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MjUxNDU3MDYsImV4cCI6MjA0MDcyMTcwNn0.yODfQfQaKeHy3c17uKfJx_wED8cwJ4TK-R4b0qoZ7pM'; const initializeSupabase = () => __awaiter(this, void 0, void 0, function* () { var _a; if ((_a = window.supabase) === null || _a === void 0 ? void 0 : _a.createClient) { // Create client with anon key first const client = window.supabase.createClient(`https://${this.projectId}.supabase.co`, anonKey); // Set custom JWT for realtime subscriptions // This allows RLS policies to work with auth.jwt()->>'wallId' // Use realtime.setAuth() for custom JWTs (not auth.setSession which is for Supabase Auth) if (this.accessToken) { try { client.realtime.setAuth(this.accessToken); this.log('Custom JWT set for realtime subscriptions'); } catch (error) { this.error('Failed to set realtime auth:', error); } } window.supabase = client; this.setupRealtimeSubscription(wallId, container); } }); // Load Supabase client if not already loaded if (!window.supabase) { const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2'; script.onload = () => { initializeSupabase(); }; document.head.appendChild(script); } else { initializeSupabase(); } } setupRealtimeSubscription(wallId, container) { if (!isBrowser) return; this.log('Setting up realtime subscription for wall:', wallId); const channelName = `wall:${wallId}`; // Use broadcast instead of postgres_changes to bypass RLS requirements // The server broadcasts new items after inserting them const subscription = window.supabase .channel(channelName) .on('broadcast', { event: 'new_item' }, (payload) => { var _a; this.log('Received broadcast:', payload); // Add the new item with animation instead of reloading everything // Try different payload structures - Supabase broadcast structure can vary const newItem = ((_a = payload === null || payload === void 0 ? void 0 : payload.payload) === null || _a === void 0 ? void 0 : _a.new) || (payload === null || payload === void 0 ? void 0 : payload.new) || (payload === null || payload === void 0 ? void 0 : payload.payload); this.log('Extracted new item:', newItem); if (newItem && newItem.content) { this.addNewItemWithAnimation(wallId, newItem, container); } else { // Fallback to full reload if payload structure is unexpected this.log('Could not extract new item, falling back to full reload'); this.loadItems(wallId, container); } }) .subscribe((status) => { this.log('Subscription status:', status); }); this.subscription = subscription; } addNewItemWithAnimation(wallId, newItem, container) { var _a; if (!isBrowser) return; const wallData = this.wallData.get(wallId); if (!wallData) { // No wall data yet, do a full load this.loadItems(wallId, container); return; } const { items, config } = wallData; // Check for duplicates if distinct_items_only is enabled if (config.distinct_items_only) { const normalizedNewContent = ((_a = newItem.content) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase()) || ''; const existingIndex = items.findIndex(item => { var _a; return (((_a = item.content) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase()) || '') === normalizedNewContent; }); if (existingIndex !== -1) { this.log('Duplicate found at index', existingIndex, '- moving to top'); // Remove the existing item from its current position items.splice(existingIndex, 1); // Also remove from DOM - find and remove the element // For ticker layout, items are inside .ticker-scroll const itemContainer = config.layout === 'ticker' ? container.querySelector('.ticker-scroll') || container : container; const existingElements = itemContainer.querySelectorAll('.wall-item'); if (existingElements[existingIndex]) { existingElements[existingIndex].remove(); } } } // Add the new item to our tracked data at the beginning items.unshift(newItem); // Enforce display limit const displayLimit = config.display_limit || 10; while (items.length > displayLimit) { items.pop(); } // Update stored data this.wallData.set(wallId, { items, config }); // Create the new item element with animation const itemElement = document.createElement('div'); itemElement.className = 'wall-item wall-item-new'; if (newItem.is_html) { itemElement.innerHTML = newItem.content; } else { itemElement.innerHTML = `<p>${newItem.content}</p>`; } // Add animation styles itemElement.style.cssText = ` opacity: 0; transform: translateY(-20px); transition: opacity 0.3s ease-out, transform 0.3s ease-out; `; // Find the right container based on layout if (config.layout === 'ticker') { const tickerScroll = container.querySelector('.ticker-scroll'); if (tickerScroll) { // For ticker, insert at the beginning (first child) tickerScroll.insertBefore(itemElement, tickerScroll.firstChild); } } else { // For list/grid layouts, insert at the beginning of container container.insertBefore(itemElement, container.firstChild); } // Trigger animation after element is in DOM requestAnimationFrame(() => { requestAnimationFrame(() => { itemElement.style.opacity = '1'; itemElement.style.transform = 'translateY(0)'; }); }); // Remove excess items from DOM if over limit if (config.layout !== 'ticker') { const allItems = container.querySelectorAll('.wall-item'); container.querySelector('.vital-status-container'); // Count items excluding status container let itemCount = 0; allItems.forEach(item => { if (!item.classList.contains('vital-status-container')) { itemCount++; if (itemCount > displayLimit) { item.remove(); } } }); } else { const tickerScroll = container.querySelector('.ticker-scroll'); if (tickerScroll) { const tickerItems = tickerScroll.querySelectorAll('.wall-item'); if (tickerItems.length > displayLimit) { // Remove from the beginning for ticker (oldest items) tickerItems[0].remove(); } } } // Clean up animation class after animation completes setTimeout(() => { itemElement.classList.remove('wall-item-new'); itemElement.style.transition = ''; }, 350); } renderItems(data, container, wallId) { const { items, config } = data; this.log('Rendering items:', (items === null || items === void 0 ? void 0 : items.length) || 0); // Store the wall data for incremental updates this.wallData.set(wallId, { items: items || [], config }); if (!items || items.length === 0) { container.innerHTML = '<div class="wall-item">No items to display</div>'; return; } container.className = `vital-wall-container ${config.layout || 'list'}-layout`; // Apply custom CSS if provided if (config.custom_css) { this.applyCustomCss(config.custom_css, wallId); } if (config.layout === 'ticker') { this.renderTickerLayout(items, container); } else { this.renderStandardLayout(items, container); } this.addStatusContainer(container); } renderTickerLayout(items, container) { if (!isBrowser) return; const viewport = document.createElement('div'); viewport.className = 'ticker-viewport'; const tickerScroll = document.createElement('div'); tickerScroll.className = 'ticker-scroll'; items.forEach(item => { const itemElement = document.createElement('div'); itemElement.className = 'wall-item'; if (item.is_html) { itemElement.innerHTML = item.content; } else { itemElement.innerHTML = `<p>${item.content}</p>`; } tickerScroll.appendChild(itemElement); }); viewport.appendChild(tickerScroll); container.innerHTML = ''; container.appendChild(viewport); } renderStandardLayout(items, container) { if (!isBrowser) return; container.innerHTML = ''; items.forEach(item => { const itemElement = document.createElement('div'); itemElement.className = 'wall-item'; if (item.is_html) { itemElement.innerHTML = item.content; } else { itemElement.innerHTML = `<p>${item.content}</p>`; } container.appendChild(itemElement); }); } addStatusContainer(container) { if (!isBrowser) return; const statusContainer = document.createElement('div'); statusContainer.className = 'vital-status-container'; const connectionStatus = document.createElement('div'); connectionStatus.className = 'vital-connection'; connectionStatus.innerHTML = ` <span class="connection-dot" style=" display: inline-block; width: 8px; height: 8px; background-color: #4CAF50; border-radius: 50%; margin-right: 4px; "></span> <span>Connected</span> `; const poweredBy = document.createElement('div'); poweredBy.className = 'vital-powered'; poweredBy.innerHTML = 'Powered by <span style="color: rgb(237, 28, 36)">Vital</span><span style="color: rgb(51, 51, 51); font-family: Cal Sans, sans-serif">Wall</span>'; poweredBy.style.cssText = ` font-weight: 500; cursor: pointer; `; poweredBy.onclick = () => { // Track the click this.trackPoweredByClick(); // Open with UTM params const url = new URL('https://vitalwall.com'); url.searchParams.set('utm_source', 'embed'); url.searchParams.set('utm_medium', 'powered_by'); url.searchParams.set('utm_campaign', this.domain); url.searchParams.set('utm_content', this.currentWallId || 'unknown'); window.open(url.toString(), '_blank'); }; statusContainer.appendChild(connectionStatus); statusContainer.appendChild(poweredBy); container.appendChild(statusContainer); } trackPoweredByClick() { // Fire and forget - don't block the link opening fetch(`${this.baseUrl}/track-powered-by-click`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ wall_id: this.currentWallId, domain: this.domain, timestamp: new Date().toISOString() }) }).catch(() => { // Silently ignore errors - tracking shouldn't affect UX }); } initializeTokens() { return __awaiter(this, void 0, void 0, function* () { this.log('Initializing tokens...'); try { const response = yield fetch(`${this.baseUrl}/generate-wall-token`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: this.apiKey, domain: this.domain }) }); this.log('Token response status:', response.status); const data = yield response.json(); this.log('Token response data:', data); if (data.error) throw new Error(data.error); this.accessToken = data.access_token; this.refreshToken = data.refresh_token; this.tokenExpiryTime = Date.now() + (data.expires_in * 1000); this.log('Tokens initialized successfully'); // Store token data (browser only) if (isBrowser) { const tokenData = { accessToken: this.accessToken, refreshToken: this.refreshToken, expiryTime: this.tokenExpiryTime, domain: this.domain, wallId: this.currentWallId || undefined }; localStorage.setItem('vitalwall-token', JSON.stringify(tokenData)); } this.scheduleTokenRefresh(); } catch (error) { this.error('Error initializing tokens:', error); throw error; } }); } scheduleTokenRefresh() { if (!this.tokenExpiryTime) return; const REFRESH_BUFFER = 5 * 60 * 1000; // 5 minutes const timeUntilRefresh = this.tokenExpiryTime - Date.now() - REFRESH_BUFFER; setTimeout(() => __awaiter(this, void 0, void 0, function* () { try { yield this.refreshAccessToken(); } catch (error) { this.error('Error refreshing token:', error); } }), Math.max(0, timeUntilRefresh)); } refreshAccessToken() { return __awaiter(this, void 0, void 0, function* () { if (!this.refreshToken) { throw new Error('No refresh token available'); } const response = yield fetch(`${this.baseUrl}/refresh-wall-token`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: this.refreshToken, domain: this.domain }) }); const data = yield response.json(); if (data.error) throw new Error(data.error); this.accessToken = data.access_token; this.tokenExpiryTime = Date.now() + (data.expires_in * 1000); // Update stored token data (browser only) if (isBrowser) { const tokenData = { accessToken: this.accessToken, refreshToken: this.refreshToken, expiryTime: this.tokenExpiryTime, domain: this.domain, wallId: this.currentWallId || undefined }; localStorage.setItem('vitalwall-token', JSON.stringify(tokenData)); } this.scheduleTokenRefresh(); }); } setupTracking() { if (!isBrowser) return; // Track existing elements const vitalItems = document.querySelectorAll('[data-vital-item]'); vitalItems.forEach(element => this.trackVitalItem(element)); // Set up observer for dynamically added elements this.setupTrackingMutationObserver(); } setupTrackingMutationObserver() { if (!isBrowser || !document.body) return; const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { const element = node; if (element.hasAttribute('data-vital-item')) { this.trackVitalItem(element); } const children = element.querySelectorAll('[data-vital-item]'); children.forEach(child => this.trackVitalItem(child)); } }); }); }); try { observer.observe(document.body, { childList: true, subtree: true }); } catch (error) { this.error('Error setting up tracking observer:', error); } } trackVitalItem(element) { if (!isBrowser) return; let content = element.getAttribute('data-vital-item') || ''; const isHtml = element.getAttribute('data-vital-html') === 'true'; const isAuto = element.getAttribute('data-vital-auto') === 'true'; const sendContentId = element.getAttribute('data-vital-send-content'); // If sendContentId is provided, find and use that element's content if (sendContentId) { const targetElement = document.getElementById(sendContentId); if (targetElement) { content = isHtml ? targetElement.innerHTML : targetElement.textContent || ''; } } // If no content specified, use the element's HTML or text if (!content) { content = isHtml ? element.innerHTML : element.textContent || ''; } // Don't track empty content if (!content) return; // Send update immediately if auto-tracking is enabled if (isAuto || (!element.getAttribute('data-vital-item') && !sendContentId)) { this.sendUpdate(content, isHtml); return; } // Otherwise, track click events element.addEventListener('click', () => { this.sendUpdate(content, isHtml); }); } sendUpdate(content, isHtml, wallId, onSuccess, onError) { const targetWallId = wallId || this.currentWallId; if (!targetWallId) { const error = new Error('No wall ID specified'); if (onError) onError(error); return; } // For real authentication, we need Bearer token if (!this.accessToken) { const error = new Error('No access token available. VitalWall may not be properly initialized.'); this.error('sendUpdate failed:', error.message); if (onError) onError(error); return; } const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.accessToken}` }; const requestBody = { content: content, isHtml: isHtml, wall_id: targetWallId, domain: this.domain }; this.log('Sending request to:', `${this.baseUrl}/wall-update`); this.log('Request headers:', headers); this.log('Request body:', requestBody); fetch(`${this.baseUrl}/wall-update`, { method: 'POST', headers: headers, body: JSON.stringify(requestBody) }) .then(response => response.json()) .then(data => { if (data.error) throw new Error(data.error); if (onSuccess) onSuccess(data.id); }) .catch(error => { this.error('Error sending update:', error); if (onError) onError(error); }); } cleanupAllWalls() { if (this.initializedWalls) { this.initializedWalls.forEach(element => { if (element._cleanup) { element._cleanup(); } element.removeAttribute('data-vital-wall-initialized'); }); this.initializedWalls.clear(); } } } // For convenient instantiation const createVitalWall = () => new VitalWall(); exports.createVitalWall = createVitalWall; exports.default = VitalWall; Object.defineProperty(exports, '__esModule', { value: true }); }));