- Remove all manual data-content-id attributes from HTML files - Archive old insertr JS/CSS assets to demo-site/archive/ - Remove hardcoded script includes and CSS links - Remove old authentication controls and mock API - Enable pure zero-config approach with class='insertr' only - Parser now generates all 40 content IDs automatically
268 lines
8.4 KiB
JavaScript
268 lines
8.4 KiB
JavaScript
/**
|
|
* Insertr Content Manager Module
|
|
* Handles content operations, storage, and API interactions
|
|
*/
|
|
|
|
class InsertrContentManager {
|
|
constructor(options = {}) {
|
|
this.options = {
|
|
apiEndpoint: options.apiEndpoint || '/api/content',
|
|
storageKey: options.storageKey || 'insertr_content',
|
|
...options
|
|
};
|
|
|
|
this.contentCache = new Map();
|
|
this.loadContentFromStorage();
|
|
}
|
|
|
|
/**
|
|
* Load content from localStorage
|
|
*/
|
|
loadContentFromStorage() {
|
|
try {
|
|
const stored = localStorage.getItem(this.options.storageKey);
|
|
if (stored) {
|
|
const data = JSON.parse(stored);
|
|
this.contentCache = new Map(Object.entries(data));
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to load content from storage:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save content to localStorage
|
|
*/
|
|
saveContentToStorage() {
|
|
try {
|
|
const data = Object.fromEntries(this.contentCache);
|
|
localStorage.setItem(this.options.storageKey, JSON.stringify(data));
|
|
} catch (error) {
|
|
console.warn('Failed to save content to storage:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get content for a specific element
|
|
* @param {string} contentId - Content identifier
|
|
* @returns {string|Object} Content data
|
|
*/
|
|
getContent(contentId) {
|
|
return this.contentCache.get(contentId);
|
|
}
|
|
|
|
/**
|
|
* Set content for an element
|
|
* @param {string} contentId - Content identifier
|
|
* @param {string|Object} content - Content data
|
|
*/
|
|
setContent(contentId, content) {
|
|
this.contentCache.set(contentId, content);
|
|
this.saveContentToStorage();
|
|
}
|
|
|
|
/**
|
|
* Apply content to DOM element
|
|
* @param {HTMLElement} element - Element to update
|
|
* @param {string|Object} content - Content to apply
|
|
*/
|
|
applyContentToElement(element, content) {
|
|
const config = element._insertrConfig;
|
|
|
|
if (config.type === 'markdown') {
|
|
// 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 = this.sanitizeForDisplay(content.text, 'text') || element.textContent;
|
|
if (content.url) {
|
|
element.href = this.sanitizeForDisplay(content.url, 'url');
|
|
}
|
|
} else if (content.text !== undefined) {
|
|
// Update text content
|
|
element.textContent = this.sanitizeForDisplay(content.text, 'text');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply markdown content to element
|
|
* @param {HTMLElement} element - Element to update
|
|
* @param {string} markdownText - Markdown content
|
|
*/
|
|
applyMarkdownContent(element, markdownText) {
|
|
// This method will be implemented by the main Insertr class
|
|
// which has access to the markdown processor
|
|
if (window.insertr && window.insertr.renderMarkdown) {
|
|
window.insertr.renderMarkdown(element, markdownText);
|
|
} else {
|
|
console.warn('Markdown processor not available');
|
|
element.textContent = markdownText;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract content from DOM element
|
|
* @param {HTMLElement} element - Element to extract from
|
|
* @returns {string|Object} Extracted content
|
|
*/
|
|
extractContentFromElement(element) {
|
|
const config = element._insertrConfig;
|
|
|
|
if (config.type === 'markdown') {
|
|
// For markdown collections, return cached content or extract text
|
|
const contentId = element.getAttribute('data-content-id');
|
|
const cached = this.contentCache.get(contentId);
|
|
|
|
if (cached) {
|
|
// Handle both old format (object) and new format (string)
|
|
if (typeof cached === 'string') {
|
|
return cached;
|
|
} else if (cached.text && typeof cached.text === 'string') {
|
|
return cached.text;
|
|
}
|
|
}
|
|
|
|
// Fallback: extract basic text content
|
|
const clone = element.cloneNode(true);
|
|
const editBtn = clone.querySelector('.insertr-edit-btn');
|
|
if (editBtn) editBtn.remove();
|
|
|
|
// Convert basic HTML structure to markdown
|
|
return this.basicHtmlToMarkdown(clone.innerHTML);
|
|
}
|
|
|
|
// Clone element to avoid modifying original
|
|
const clone = element.cloneNode(true);
|
|
|
|
// Remove edit button from clone
|
|
const editBtn = clone.querySelector('.insertr-edit-btn');
|
|
if (editBtn) editBtn.remove();
|
|
|
|
// Extract content based on element type
|
|
if (config.type === 'link' && config.includeUrl) {
|
|
return {
|
|
text: clone.textContent.trim(),
|
|
url: element.href || ''
|
|
};
|
|
}
|
|
|
|
return clone.textContent.trim();
|
|
}
|
|
|
|
/**
|
|
* Convert basic HTML to markdown (for initial content extraction)
|
|
* @param {string} html - HTML content
|
|
* @returns {string} Markdown content
|
|
*/
|
|
basicHtmlToMarkdown(html) {
|
|
let markdown = html;
|
|
|
|
// Basic paragraph conversion
|
|
markdown = markdown.replace(/<p[^>]*>(.*?)<\/p>/gis, (match, content) => {
|
|
return content.trim() + '\n\n';
|
|
});
|
|
markdown = markdown.replace(/<br\s*\/?>/gi, '\n');
|
|
|
|
// Remove HTML tags
|
|
markdown = markdown.replace(/<[^>]*>/g, '');
|
|
|
|
// Clean up entities
|
|
markdown = markdown.replace(/ /g, ' ');
|
|
markdown = markdown.replace(/&/g, '&');
|
|
markdown = markdown.replace(/</g, '<');
|
|
markdown = markdown.replace(/>/g, '>');
|
|
|
|
// Clean whitespace
|
|
markdown = markdown
|
|
.split('\n')
|
|
.map(line => line.trim())
|
|
.join('\n')
|
|
.replace(/\n\n\n+/g, '\n\n')
|
|
.trim();
|
|
|
|
return markdown;
|
|
}
|
|
|
|
/**
|
|
* Save content to server (mock implementation)
|
|
* @param {string} contentId - Content identifier
|
|
* @param {string|Object} content - Content to save
|
|
* @returns {Promise} Save operation promise
|
|
*/
|
|
async saveToServer(contentId, content) {
|
|
// Mock API call - replace with real implementation
|
|
try {
|
|
console.log(`💾 Saving content for ${contentId}:`, content);
|
|
|
|
// Simulate network delay
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
// Store locally for now
|
|
this.setContent(contentId, content);
|
|
|
|
return { success: true, contentId, content };
|
|
} catch (error) {
|
|
console.error('Failed to save content:', error);
|
|
throw new Error('Save operation failed');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Basic sanitization for display
|
|
* @param {string} content - Content to sanitize
|
|
* @param {string} type - Content type
|
|
* @returns {string} Sanitized content
|
|
*/
|
|
sanitizeForDisplay(content, type) {
|
|
if (!content) return '';
|
|
|
|
switch (type) {
|
|
case 'text':
|
|
return this.escapeHtml(content);
|
|
case 'url':
|
|
if (content.startsWith('javascript:') || content.startsWith('data:')) {
|
|
return '';
|
|
}
|
|
return content;
|
|
default:
|
|
return this.escapeHtml(content);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Escape HTML characters
|
|
* @param {string} text - Text to escape
|
|
* @returns {string} Escaped text
|
|
*/
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
/**
|
|
* Clear all cached content
|
|
*/
|
|
clearCache() {
|
|
this.contentCache.clear();
|
|
localStorage.removeItem(this.options.storageKey);
|
|
}
|
|
|
|
/**
|
|
* Get all cached content
|
|
* @returns {Object} All cached content
|
|
*/
|
|
getAllContent() {
|
|
return Object.fromEntries(this.contentCache);
|
|
}
|
|
}
|
|
|
|
// Export for module usage
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = InsertrContentManager;
|
|
}
|
|
|
|
// Global export for browser usage
|
|
if (typeof window !== 'undefined') {
|
|
window.InsertrContentManager = InsertrContentManager;
|
|
} |