diff --git a/demo-site/about.html b/demo-site/about.html index 1836afc..6de205b 100644 --- a/demo-site/about.html +++ b/demo-site/about.html @@ -110,6 +110,7 @@ + \ No newline at end of file diff --git a/demo-site/index.html b/demo-site/index.html index 0fa1aa0..74f4d9d 100644 --- a/demo-site/index.html +++ b/demo-site/index.html @@ -86,6 +86,7 @@ + \ No newline at end of file diff --git a/demo-site/insertr/insertr.css b/demo-site/insertr/insertr.css index f19f399..7a0ad46 100644 --- a/demo-site/insertr/insertr.css +++ b/demo-site/insertr/insertr.css @@ -233,4 +233,36 @@ .insertr-auth-status.edit-mode { background: #3b82f6; +} + +/* Validation messages */ +.insertr-validation-message { + margin-top: 0.5rem; + padding: 0.5rem; + border-radius: 4px; + font-size: 0.875rem; + animation: slideIn 0.3s ease-out; +} + +.insertr-validation-message.error { + background-color: #fef2f2; + border: 1px solid #fecaca; + color: #dc2626; +} + +.insertr-validation-message.success { + background-color: #f0fdf4; + border: 1px solid #bbf7d0; + color: #16a34a; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } } \ No newline at end of file diff --git a/demo-site/insertr/insertr.js b/demo-site/insertr/insertr.js index cf7a87c..12ef503 100644 --- a/demo-site/insertr/insertr.js +++ b/demo-site/insertr/insertr.js @@ -45,6 +45,9 @@ class Insertr { // Initialize markdown support this.initializeMarkdown(); + + // Initialize client-side validation + this.initializeValidation(); } initializeMarkdown() { @@ -74,6 +77,17 @@ class Insertr { } } + initializeValidation() { + // Check if DOMPurify is available + if (typeof DOMPurify === 'undefined' && typeof window.DOMPurify === 'undefined') { + console.warn('DOMPurify not loaded! Client-side validation will be limited.'); + return; + } + + this.DOMPurify = window.DOMPurify || DOMPurify; + console.log('✅ DOMPurify loaded for client-side validation'); + } + async init() { console.log('🚀 Insertr initializing with element-level editing...'); @@ -292,6 +306,9 @@ class Insertr { this.saveElementContent(contentId, form); }); + // Add real-time validation + this.setupFormValidation(form, config); + // Focus on first input setTimeout(() => { const firstInput = form.querySelector('input, textarea'); @@ -306,6 +323,51 @@ class Insertr { return form; } + setupFormValidation(form, config) { + const inputs = form.querySelectorAll('input, textarea'); + + inputs.forEach(input => { + let fieldType; + + // Determine field type for validation + if (input.name === 'url') { + fieldType = 'link'; + } else if (config.type === 'markdown') { + fieldType = 'markdown'; + } else if (input.tagName === 'TEXTAREA') { + fieldType = 'textarea'; + } else { + fieldType = 'text'; + } + + // Real-time validation on input + input.addEventListener('input', () => { + // Clear previous validation messages + const existingMsg = form.querySelector('.insertr-validation-message'); + if (existingMsg) { + existingMsg.remove(); + } + + // Only validate if there's content and it's not just whitespace + const value = input.value.trim(); + if (value.length > 10) { // Only validate after some content is entered + const validation = this.validateInput(value, fieldType); + if (!validation.valid) { + this.showValidationMessage(input, validation.message); + } + } + }); + + // Clear validation message when user focuses to fix issues + input.addEventListener('focus', () => { + const existingMsg = form.querySelector('.insertr-validation-message.error'); + if (existingMsg) { + existingMsg.remove(); + } + }); + }); + } + showEditForm(element, form) { // Hide edit button during editing const editBtn = element.querySelector('.insertr-edit-btn'); @@ -348,12 +410,43 @@ class Insertr { // For markdown, store just the string directly const input = form.querySelector('textarea[name="content"]'); newContent = input.value; + + // Validate markdown content + const validation = this.validateInput(newContent, 'markdown'); + if (!validation.valid) { + this.showValidationMessage(input, validation.message); + return; + } } else if (config.type === 'link' && config.includeUrl) { - newContent.text = form.querySelector('input[name="text"]').value; - newContent.url = form.querySelector('input[name="url"]').value; + const textInput = form.querySelector('input[name="text"]'); + const urlInput = form.querySelector('input[name="url"]'); + + newContent.text = textInput.value; + newContent.url = urlInput.value; + + // Validate text and URL + const textValidation = this.validateInput(newContent.text, 'text'); + if (!textValidation.valid) { + this.showValidationMessage(textInput, textValidation.message); + return; + } + + const urlValidation = this.validateInput(newContent.url, 'link'); + if (!urlValidation.valid) { + this.showValidationMessage(urlInput, urlValidation.message); + return; + } } else { const input = form.querySelector('input[name="content"], textarea[name="content"]'); newContent.text = input.value; + + // Validate text content + const fieldType = config.type === 'textarea' ? 'textarea' : 'text'; + const validation = this.validateInput(newContent.text, fieldType); + if (!validation.valid) { + this.showValidationMessage(input, validation.message); + return; + } } // Add saving state @@ -394,14 +487,14 @@ class Insertr { // Handle markdown collection - content is a string this.applyMarkdownContent(element, content); } else if (config.type === 'link' && config.includeUrl && content.url !== undefined) { - // Update link text and URL - element.textContent = content.text || element.textContent; + // Update link text and URL with basic sanitization + element.textContent = this.sanitizeForDisplay(content.text, 'text') || element.textContent; if (content.url) { - element.href = content.url; + element.href = this.sanitizeForDisplay(content.url, 'url'); } } else if (content.text !== undefined) { - // Update text content - element.textContent = content.text; + // Update text content with basic sanitization + element.textContent = this.sanitizeForDisplay(content.text, 'text'); } } @@ -429,11 +522,19 @@ class Insertr { return; } + // Basic sanitization for display (allow common formatting tags) + const sanitizedHtml = this.DOMPurify ? + this.DOMPurify.sanitize(html, { + ALLOWED_TAGS: ['p', 'strong', 'em', 'a', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'br'], + ALLOWED_ATTR: ['href', 'class'], + ALLOWED_SCHEMES: ['http', 'https', 'mailto'] + }) : html; + // Store original edit button const editBtn = element.querySelector('.insertr-edit-btn'); // Update element content - element.innerHTML = html; + element.innerHTML = sanitizedHtml; // Re-add edit button if (editBtn) { @@ -641,6 +742,128 @@ class Insertr { return div.innerHTML; } + // Client-side validation (UX focused, not security) + validateInput(input, fieldType) { + if (!input || typeof input !== 'string') { + return { valid: false, message: 'Content cannot be empty' }; + } + + // Basic length validation + if (input.length > 10000) { + return { valid: false, message: 'Content is too long (max 10,000 characters)' }; + } + + // Field-specific validation + switch (fieldType) { + case 'text': + return this.validateTextInput(input); + case 'textarea': + return this.validateTextInput(input); + case 'link': + return this.validateLinkInput(input); + case 'markdown': + return this.validateMarkdownInput(input); + default: + return { valid: true }; + } + } + + validateTextInput(input) { + // Check for obvious HTML that users might accidentally include + if (input.includes('')) { + return { valid: false, message: 'Script tags are not allowed for security reasons' }; + } + + if (input.includes('<') && input.includes('>')) { + return { + valid: false, + message: 'HTML tags are not allowed in text fields. Use markdown collections for formatted content.' + }; + } + + return { valid: true }; + } + + validateLinkInput(input) { + // Basic URL validation for user feedback + const urlPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/; + if (input.startsWith('http') && !urlPattern.test(input)) { + return { valid: false, message: 'Please enter a valid URL (e.g., https://example.com)' }; + } + + return { valid: true }; + } + + validateMarkdownInput(input) { + // Check for potentially problematic content + if (input.includes('