UNPKG

armor-editor

Version:

Advanced rich text editor with premium armor-grade security, real-time collaboration, spell checking, track changes, and framework-agnostic design for React, Vue, Angular, Next.js, Nuxt.js

1,270 lines (1,249 loc) 347 kB
'use strict'; const icons = { bold: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z"/></svg>`, italic: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z"/></svg>`, underline: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 17c3.31 0 6-2.69 6-6V3h-2.5v8c0 1.93-1.57 3.5-3.5 3.5S8.5 12.93 8.5 11V3H6v8c0 3.31 2.69 6 6 6zm-7 2v2h14v-2H5z"/></svg>`, strikethrough: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M10 19h4v-3h-4v3zM5 4v3h5v3h4V7h5V4H5zM3 14h18v-2H3v2z"/></svg>`, alignLeft: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M15 15H3v2h12v-2zm0-8H3v2h12V7zM3 13h18v-2H3v2zm0 8h18v-2H3v2zM3 3v2h18V3H3z"/></svg>`, alignCenter: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M7 15v2h10v-2H7zm-4 6h18v-2H3v2zm0-8h18v-2H3v2zm4-6v2h10V7H7zM3 3v2h18V3H3z"/></svg>`, alignRight: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M3 21h18v-2H3v2zm6-4h12v-2H9v2zm-6-4h18v-2H3v2zm6-4h12V7H9v2zM3 3v2h18V3H3z"/></svg>`, alignJustify: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M3 21h18v-2H3v2zm0-4h18v-2H3v2zm0-4h18v-2H3v2zm0-4h18V7H3v2zm0-6v2h18V3H3z"/></svg>`, orderedList: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z"/></svg>`, unorderedList: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/></svg>`, indent: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M3 21h18v-2H3v2zM3 8v8l4-4-4-4zm8 9h10v-2H11v2zM3 3v2h18V3H3zm8 6h10V7H11v2zm0 4h10v-2H11v2z"/></svg>`, outdent: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M11 17h10v-2H11v2zm-8-5l4 4V8l-4 4zm0 9h18v-2H3v2zM3 3v2h18V3H3zm8 6h10V7H11v2zm0 4h10v-2H11v2z"/></svg>`, link: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>`, image: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>`, table: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M10 10.02h5V21h-5zM17 21h3c1.1 0 2-.9 2-2v-9h-5v11zm3-18H5c-1.1 0-2 .9-2 2v3h19V5c0-1.1-.9-2-2-2zM3 19c0 1.1.9 2 2 2h3V10H3v9z"/></svg>`, code: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0L19.2 12l-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>`, blockquote: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg>`, undo: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z"/></svg>`, redo: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M18.4 10.6C16.55 8.99 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z"/></svg>`, removeFormat: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M6 5v.18L8.82 8h2.4l-.72 1.68 2.1 2.1L14.21 8H20V5H6zm-.27 14.49L17.73 7.49l1.41 1.41L7.14 20.9l-1.41-1.41z"/></svg>`, fullscreen: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>`, textColor: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M9.62 12L12 5.67 14.38 12H9.62zM11 3L5.5 17h2.25l1.12-3h6.25l1.12 3H18.5L13 3H11zM3 20h18v3H3v-3z"/></svg>`, backgroundColor: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M16.56 8.94L7.62 0 6.21 1.41l2.38 2.38-5.15 5.15c-.59.59-.59 1.54 0 2.12l5.5 5.5c.29.29.68.44 1.06.44s.77-.15 1.06-.44l5.5-5.5c.59-.58.59-1.53 0-2.12zM5.21 10L10 5.21 14.79 10H5.21zM19 11.5s-2 2.17-2 3.5c0 1.1.9 2 2 2s2-.9 2-2c0-1.33-2-3.5-2-3.5z"/><path d="M2 20h20v4H2z"/></svg>`, trackChanges: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M14.06 9.02l.92.92L5.92 19H5v-.92l9.06-9.06M17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75z"/></svg>`, comments: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M21.99 4c0-1.1-.89-2-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4-.01-18zM18 14H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/></svg>`, spellCheck: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12.45 16h2.09L9.43 3H7.57L2.46 16h2.09l1.12-3h5.64l1.14 3zm-6.02-5L8.5 5.48 10.57 11H6.43zm15.16.59l-8.09 8.09L9.83 16l-1.41 1.41 5.09 5.09L23 13l-1.41-1.41z"/></svg>`, mathType: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 11.5c0 2-1.5 3.5-3.5 3.5s-3.5-1.5-3.5-3.5S10 8 12 8s3.5 1.5 3.5 3.5zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8 0-1.12.23-2.18.65-3.15L8 11.5c0 2.21 1.79 4 4 4s4-1.79 4-4-1.79-4-4-4c-.63 0-1.22.15-1.76.4l-2.35-2.35C9.82 4.23 10.88 4 12 4c4.41 0 8 3.59 8 8s-3.59 8-8 8z"/></svg>`, mediaEmbed: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4zM14 13h-3v3H9v-3H6v-2h3V8h2v3h3v2z"/></svg>`, mentions: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C13.1 2 14 2.9 14 4C14 5.1 13.1 6 12 6C10.9 6 10 5.1 10 4C10 2.9 10.9 2 12 2ZM21 9V7L15 1L13.5 2.5L16.17 5.17L10.58 10.76C10.22 10.54 9.8 10.38 9.35 10.35L8.8 10.3C8.32 10.25 7.86 10.39 7.5 10.68L4.5 13.68C4.15 14.03 4 14.53 4 15.03V19C4 20.1 4.9 21 6 21H18C19.1 21 20 20.1 20 19V15C20 14.45 19.55 14 19 14S18 14.45 18 15V19H6V15.03L9 12.03L9.56 12.08C10.07 12.13 10.54 12.35 10.89 12.71L12.89 14.71L15.71 11.89C16.1 11.5 16.1 10.87 15.71 10.47L12.53 7.29L21 9Z"/></svg>`, exportPdf: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/></svg>`, exportWord: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M7,13V15H9V13H7M11,13V15H13V13H11M15,13V15H17V13H15Z"/></svg>`, importWord: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M12,12L16,16H13.5V19H10.5V16H8L12,12Z"/></svg>`, wordCount: `<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M3,3V21H21V3H3M5,5H19V19H5V5M7,7V9H17V7H7M7,11V13H17V11H7M7,15V17H14V15H7Z"/></svg>` }; const themes = { light: { name: 'Light', colors: { background: '#ffffff', text: '#333333', border: '#e1e5e9', toolbar: '#f8f9fa', button: '#ffffff', buttonHover: '#e9ecef', selection: '#007cba', accent: '#007cba' }, fonts: { primary: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', monospace: 'Monaco, Menlo, "Ubuntu Mono", monospace' } }, dark: { name: 'Dark', colors: { background: '#1a1a1a', text: '#e1e1e1', border: '#404040', toolbar: '#2d2d2d', button: '#404040', buttonHover: '#505050', selection: '#0ea5e9', accent: '#0ea5e9' }, fonts: { primary: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', monospace: 'Monaco, Menlo, "Ubuntu Mono", monospace' } }, minimal: { name: 'Minimal', colors: { background: '#fefefe', text: '#2c2c2c', border: '#f0f0f0', toolbar: '#fbfbfb', button: 'transparent', buttonHover: '#f5f5f5', selection: '#6366f1', accent: '#6366f1' }, fonts: { primary: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif', monospace: 'JetBrains Mono, Monaco, monospace' } } }; function applyTheme(container, theme) { const style = ` --ae-bg: ${theme.colors.background}; --ae-text: ${theme.colors.text}; --ae-border: ${theme.colors.border}; --ae-toolbar: ${theme.colors.toolbar}; --ae-button: ${theme.colors.button}; --ae-button-hover: ${theme.colors.buttonHover}; --ae-selection: ${theme.colors.selection}; --ae-accent: ${theme.colors.accent}; --ae-font-primary: ${theme.fonts.primary}; --ae-font-mono: ${theme.fonts.monospace}; `; container.style.cssText += style; } class PluginManager { constructor(editor) { this.plugins = new Map(); this.editor = editor; } register(plugin) { if (this.plugins.has(plugin.name)) { console.warn(`Plugin ${plugin.name} is already registered`); return; } this.plugins.set(plugin.name, plugin); plugin.init(this.editor); } unregister(pluginName) { const plugin = this.plugins.get(pluginName); if (plugin && plugin.destroy) { plugin.destroy(); } this.plugins.delete(pluginName); } getPlugin(name) { return this.plugins.get(name); } getAllPlugins() { return Array.from(this.plugins.values()); } destroy() { this.plugins.forEach(plugin => { if (plugin.destroy) { plugin.destroy(); } }); this.plugins.clear(); } } // Built-in plugins const wordCountPlugin = { name: 'wordCount', version: '1.0.0', init: (editor) => { const updateWordCount = () => { const text = editor.getText(); const words = text.trim().split(/\s+/).filter((word) => word.length > 0).length; const chars = text.length; let counter = editor.container.querySelector('.word-counter'); if (!counter) { counter = document.createElement('div'); counter.className = 'word-counter'; counter.style.cssText = ` position: absolute; bottom: 5px; right: 10px; font-size: 12px; color: #666; background: rgba(255,255,255,0.9); padding: 2px 6px; border-radius: 3px; `; editor.container.appendChild(counter); } counter.textContent = `${words} words, ${chars} chars`; }; editor.on('change', updateWordCount); updateWordCount(); } }; const autoSavePlugin = { name: 'autoSave', version: '1.0.0', init: (editor) => { let timeout; const autoSave = () => { clearTimeout(timeout); timeout = setTimeout(() => { const content = editor.getContent(); localStorage.setItem(`armor-editor-${editor.id}`, content); console.log('Content auto-saved'); }, 2000); }; editor.on('change', autoSave); } }; class ShortcutManager { constructor(editor) { this.shortcuts = new Map(); this.editor = editor; this.setupEventListener(); this.registerDefaultShortcuts(); } setupEventListener() { this.editor.editor.addEventListener('keydown', (e) => { const shortcutKey = this.getShortcutKey(e); const shortcut = this.shortcuts.get(shortcutKey); if (shortcut) { e.preventDefault(); shortcut.action(); } }); } getShortcutKey(e) { const parts = []; if (e.ctrlKey) parts.push('ctrl'); if (e.altKey) parts.push('alt'); if (e.shiftKey) parts.push('shift'); if (e.metaKey) parts.push('meta'); parts.push(e.key.toLowerCase()); return parts.join('+'); } register(shortcut) { const key = this.buildShortcutKey(shortcut); this.shortcuts.set(key, shortcut); } buildShortcutKey(shortcut) { const parts = []; if (shortcut.ctrl) parts.push('ctrl'); if (shortcut.alt) parts.push('alt'); if (shortcut.shift) parts.push('shift'); if (shortcut.meta) parts.push('meta'); parts.push(shortcut.key.toLowerCase()); return parts.join('+'); } registerDefaultShortcuts() { const shortcuts = [ { key: 'b', ctrl: true, action: () => document.execCommand('bold'), description: 'Bold text' }, { key: 'i', ctrl: true, action: () => document.execCommand('italic'), description: 'Italic text' }, { key: 'u', ctrl: true, action: () => document.execCommand('underline'), description: 'Underline text' }, { key: 'z', ctrl: true, action: () => document.execCommand('undo'), description: 'Undo' }, { key: 'y', ctrl: true, action: () => document.execCommand('redo'), description: 'Redo' }, { key: 's', ctrl: true, action: () => { var _a; if ((_a = this.editor.options.autoSave) === null || _a === void 0 ? void 0 : _a.callback) { this.editor.options.autoSave.callback(this.editor.getContent()); } }, description: 'Save' } ]; shortcuts.forEach(shortcut => this.register(shortcut)); } getShortcuts() { return Array.from(this.shortcuts.values()); } } // Performance optimization utilities class PerformanceMonitor { constructor() { this.metrics = new Map(); this.observer = null; if (typeof PerformanceObserver !== 'undefined') { this.observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { this.recordMetric(entry.name, entry.duration); } }); this.observer.observe({ entryTypes: ['measure'] }); } } startMeasure(name) { performance.mark(`${name}-start`); } endMeasure(name) { performance.mark(`${name}-end`); performance.measure(name, `${name}-start`, `${name}-end`); } recordMetric(name, value) { if (!this.metrics.has(name)) { this.metrics.set(name, []); } const values = this.metrics.get(name); values.push(value); // Keep only last 100 measurements if (values.length > 100) { values.shift(); } } getMetrics() { const result = {}; this.metrics.forEach((values, name) => { const avg = values.reduce((a, b) => a + b, 0) / values.length; const min = Math.min(...values); const max = Math.max(...values); result[name] = { average: Math.round(avg * 100) / 100, min: Math.round(min * 100) / 100, max: Math.round(max * 100) / 100, count: values.length }; }); return result; } destroy() { if (this.observer) { this.observer.disconnect(); } this.metrics.clear(); } } function debounce(func, wait) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func.apply(null, args), wait); }; } function throttle(func, limit) { let inThrottle; return (...args) => { if (!inThrottle) { func.apply(null, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } class VirtualScroll { constructor(container, itemHeight, totalItems, renderItem) { this.scrollTop = 0; this.container = container; this.itemHeight = itemHeight; this.totalItems = totalItems; this.renderItem = renderItem; this.visibleItems = Math.ceil(container.clientHeight / itemHeight) + 2; this.setupScrolling(); } setupScrolling() { this.container.style.overflowY = 'auto'; this.container.style.height = `${this.totalItems * this.itemHeight}px`; const throttledScroll = throttle(() => { this.scrollTop = this.container.scrollTop; this.render(); }, 16); this.container.addEventListener('scroll', throttledScroll); this.render(); } render() { const startIndex = Math.floor(this.scrollTop / this.itemHeight); const endIndex = Math.min(startIndex + this.visibleItems, this.totalItems); // Clear existing items this.container.innerHTML = ''; // Create spacer for items above viewport if (startIndex > 0) { const spacer = document.createElement('div'); spacer.style.height = `${startIndex * this.itemHeight}px`; this.container.appendChild(spacer); } // Render visible items for (let i = startIndex; i < endIndex; i++) { const item = this.renderItem(i); this.container.appendChild(item); } // Create spacer for items below viewport const remainingItems = this.totalItems - endIndex; if (remainingItems > 0) { const spacer = document.createElement('div'); spacer.style.height = `${remainingItems * this.itemHeight}px`; this.container.appendChild(spacer); } } } class CommandPalette { constructor(editor) { this.commands = new Map(); this.palette = null; this.input = null; this.results = null; this.isVisible = false; this.editor = editor; this.registerDefaultCommands(); this.setupKeyListener(); } setupKeyListener() { document.addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'P') { e.preventDefault(); this.toggle(); } if (e.key === 'Escape' && this.isVisible) { this.hide(); } }); } registerDefaultCommands() { const commands = [ { id: 'bold', title: 'Bold', description: 'Make text bold', category: 'Format', shortcut: 'Ctrl+B', action: () => document.execCommand('bold') }, { id: 'italic', title: 'Italic', description: 'Make text italic', category: 'Format', shortcut: 'Ctrl+I', action: () => document.execCommand('italic') }, { id: 'theme-dark', title: 'Switch to Dark Theme', category: 'Theme', action: () => this.editor.setTheme('dark') }, { id: 'theme-light', title: 'Switch to Light Theme', category: 'Theme', action: () => this.editor.setTheme('light') }, { id: 'save', title: 'Save Document', category: 'File', shortcut: 'Ctrl+S', action: () => { var _a, _b; return (_b = (_a = this.editor.options.autoSave) === null || _a === void 0 ? void 0 : _a.callback) === null || _b === void 0 ? void 0 : _b.call(_a, this.editor.getContent()); } } ]; commands.forEach(cmd => this.registerCommand(cmd)); } registerCommand(command) { this.commands.set(command.id, command); } createPalette() { this.palette = document.createElement('div'); this.palette.className = 'command-palette'; this.palette.style.cssText = ` position: fixed; top: 20%; left: 50%; transform: translateX(-50%); width: 600px; max-width: 90vw; background: var(--ae-bg, white); border: 1px solid var(--ae-border, #ccc); border-radius: 8px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); z-index: 10000; overflow: hidden; `; this.input = document.createElement('input'); this.input.placeholder = 'Type a command...'; this.input.style.cssText = ` width: 100%; padding: 16px; border: none; outline: none; font-size: 16px; background: transparent; color: var(--ae-text, black); `; this.results = document.createElement('div'); this.results.style.cssText = ` max-height: 300px; overflow-y: auto; `; this.palette.appendChild(this.input); this.palette.appendChild(this.results); this.input.addEventListener('input', () => this.updateResults()); this.input.addEventListener('keydown', (e) => this.handleKeydown(e)); document.body.appendChild(this.palette); } updateResults() { if (!this.input || !this.results) return; const query = this.input.value.toLowerCase(); const filtered = Array.from(this.commands.values()) .filter(cmd => cmd.title.toLowerCase().includes(query) || cmd.category.toLowerCase().includes(query)) .slice(0, 10); this.results.innerHTML = ''; filtered.forEach((cmd, index) => { const item = document.createElement('div'); item.className = `command-item ${index === 0 ? 'selected' : ''}`; item.style.cssText = ` padding: 12px 16px; cursor: pointer; border-bottom: 1px solid var(--ae-border, #eee); display: flex; justify-content: space-between; align-items: center; `; item.innerHTML = ` <div> <div style="font-weight: 500;">${cmd.title}</div> <div style="font-size: 12px; color: #666;">${cmd.category}</div> </div> ${cmd.shortcut ? `<kbd style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 11px;">${cmd.shortcut}</kbd>` : ''} `; item.addEventListener('click', () => { cmd.action(); this.hide(); }); this.results.appendChild(item); }); } handleKeydown(e) { var _a, _b; const items = (_a = this.results) === null || _a === void 0 ? void 0 : _a.querySelectorAll('.command-item'); if (!items) return; const selected = (_b = this.results) === null || _b === void 0 ? void 0 : _b.querySelector('.selected'); let index = Array.from(items).indexOf(selected); if (e.key === 'ArrowDown') { e.preventDefault(); index = Math.min(index + 1, items.length - 1); } else if (e.key === 'ArrowUp') { e.preventDefault(); index = Math.max(index - 1, 0); } else if (e.key === 'Enter') { e.preventDefault(); selected === null || selected === void 0 ? void 0 : selected.click(); return; } items.forEach((item, i) => { item.classList.toggle('selected', i === index); }); } show() { var _a; if (!this.palette) this.createPalette(); if (this.palette) { this.palette.style.display = 'block'; (_a = this.input) === null || _a === void 0 ? void 0 : _a.focus(); this.updateResults(); this.isVisible = true; } } hide() { if (this.palette) { this.palette.style.display = 'none'; this.isVisible = false; } } toggle() { this.isVisible ? this.hide() : this.show(); } destroy() { if (this.palette) { document.body.removeChild(this.palette); } } } // Enhanced Mobile Support class MobileEnhancements { constructor(editor) { this.touchStartX = 0; this.touchStartY = 0; this.isSelecting = false; this.editor = editor; this.setupTouchHandlers(); this.setupVirtualKeyboard(); } setupTouchHandlers() { const editorElement = this.editor.editor; // Touch selection editorElement.addEventListener('touchstart', (e) => { this.touchStartX = e.touches[0].clientX; this.touchStartY = e.touches[0].clientY; }); editorElement.addEventListener('touchmove', (e) => { const touch = e.touches[0]; const deltaX = Math.abs(touch.clientX - this.touchStartX); const deltaY = Math.abs(touch.clientY - this.touchStartY); if (deltaX > 10 || deltaY > 10) { this.isSelecting = true; } }); // Double tap to select word let lastTap = 0; editorElement.addEventListener('touchend', (e) => { const currentTime = new Date().getTime(); const tapLength = currentTime - lastTap; if (tapLength < 500 && tapLength > 0) { this.selectWordAtPoint(e.changedTouches[0]); e.preventDefault(); } lastTap = currentTime; this.isSelecting = false; }); // Pinch to zoom (for large documents) let initialDistance = 0; editorElement.addEventListener('touchstart', (e) => { if (e.touches.length === 2) { initialDistance = this.getDistance(e.touches[0], e.touches[1]); } }); editorElement.addEventListener('touchmove', (e) => { if (e.touches.length === 2) { const currentDistance = this.getDistance(e.touches[0], e.touches[1]); const scale = currentDistance / initialDistance; if (scale > 1.2) { this.increaseFontSize(); initialDistance = currentDistance; } else if (scale < 0.8) { this.decreaseFontSize(); initialDistance = currentDistance; } } }); } setupVirtualKeyboard() { // Handle virtual keyboard appearance const viewport = document.querySelector('meta[name=viewport]'); if (viewport) { viewport.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'); } // Adjust editor height when keyboard appears window.addEventListener('resize', () => { if (this.isVirtualKeyboardOpen()) { this.adjustForKeyboard(); } }); } selectWordAtPoint(touch) { const range = document.caretRangeFromPoint(touch.clientX, touch.clientY); if (range) { const selection = window.getSelection(); if (selection) { // Expand to word boundaries (fallback for browsers without expand) try { range.expand('word'); } catch (_a) { // Fallback: select current word manually const text = range.toString(); if (text) { const startContainer = range.startContainer; const textContent = startContainer.textContent || ''; const offset = range.startOffset; // Find word boundaries let start = offset; let end = offset; while (start > 0 && /\w/.test(textContent[start - 1])) start--; while (end < textContent.length && /\w/.test(textContent[end])) end++; range.setStart(startContainer, start); range.setEnd(startContainer, end); } } selection.removeAllRanges(); selection.addRange(range); // Show formatting toolbar this.showMobileToolbar(); } } } showMobileToolbar() { const selection = window.getSelection(); if (!selection || selection.isCollapsed) return; const toolbar = document.createElement('div'); toolbar.className = 'mobile-format-toolbar'; toolbar.style.cssText = ` position: absolute; background: #333; color: white; padding: 8px; border-radius: 6px; display: flex; gap: 8px; z-index: 1000; box-shadow: 0 2px 10px rgba(0,0,0,0.3); `; const buttons = [ { text: 'B', action: () => document.execCommand('bold') }, { text: 'I', action: () => document.execCommand('italic') }, { text: 'U', action: () => document.execCommand('underline') } ]; buttons.forEach(btn => { const button = document.createElement('button'); button.textContent = btn.text; button.style.cssText = ` background: transparent; border: 1px solid #555; color: white; padding: 6px 10px; border-radius: 4px; font-weight: bold; `; button.addEventListener('click', btn.action); toolbar.appendChild(button); }); // Position toolbar above selection const rect = selection.getRangeAt(0).getBoundingClientRect(); toolbar.style.left = `${rect.left}px`; toolbar.style.top = `${rect.top - 50}px`; document.body.appendChild(toolbar); // Remove toolbar after 3 seconds or on next touch setTimeout(() => toolbar.remove(), 3000); document.addEventListener('touchstart', () => toolbar.remove(), { once: true }); } getDistance(touch1, touch2) { const dx = touch1.clientX - touch2.clientX; const dy = touch1.clientY - touch2.clientY; return Math.sqrt(dx * dx + dy * dy); } increaseFontSize() { const currentSize = parseInt(getComputedStyle(this.editor.editor).fontSize); this.editor.editor.style.fontSize = `${Math.min(currentSize + 2, 24)}px`; } decreaseFontSize() { const currentSize = parseInt(getComputedStyle(this.editor.editor).fontSize); this.editor.editor.style.fontSize = `${Math.max(currentSize - 2, 12)}px`; } isVirtualKeyboardOpen() { return window.innerHeight < screen.height * 0.75; } adjustForKeyboard() { const keyboardHeight = screen.height - window.innerHeight; this.editor.container.style.paddingBottom = `${keyboardHeight}px`; } } class AIEnhancements { constructor(editor, config) { this.suggestionBox = null; this.templates = []; this.editor = editor; this.config = config; this.initializeTemplates(); if (config.enabled) { this.setupSmartSuggestions(); } } initializeTemplates() { this.templates = [ { id: 'blog-post', name: 'Blog Post', description: 'Professional blog post template', category: 'Content', content: '# {{title}}\n\n{{introduction}}\n\n## Main Content\n{{main_points}}\n\n## Conclusion\n{{conclusion}}\n\n---\n*Published on {{date}}*', variables: ['title', 'introduction', 'main_points', 'conclusion', 'date'] }, { id: 'meeting-notes', name: 'Meeting Notes', description: 'Structured meeting notes template', category: 'Business', content: '# Meeting Notes - {{meeting_title}}\n\n**Date:** {{date}}\n**Attendees:** {{attendees}}\n**Duration:** {{duration}}\n\n## Agenda\n{{agenda_items}}\n\n## Discussion Points\n{{discussion}}\n\n## Action Items\n{{action_items}}\n\n## Next Steps\n{{next_steps}}', variables: ['meeting_title', 'date', 'attendees', 'duration', 'agenda_items', 'discussion', 'action_items', 'next_steps'] } ]; } setupSmartSuggestions() { let typingTimer; this.editor.editor.addEventListener('input', () => { clearTimeout(typingTimer); typingTimer = setTimeout(() => { this.checkForSuggestions(); }, 1000); }); // Template trigger this.editor.editor.addEventListener('keydown', (e) => { if (e.key === '/' && this.editor.getText().trim() === '') { e.preventDefault(); this.showTemplateSelector(); } }); } async checkForSuggestions() { try { const content = this.editor.getText(); if (content.length < 10) return; const suggestions = await this.getAISuggestions(content); if (suggestions.length > 0) { this.showSuggestions(suggestions); } } catch (error) { console.error('Failed to check for AI suggestions:', error); } } async getAISuggestions(content) { try { // Simulate AI suggestions (in real implementation, call AI API) const suggestions = [ 'Consider adding more specific examples', 'This paragraph could be more concise', 'Add a transition sentence here' ]; return suggestions.slice(0, 2); // Return top 2 suggestions } catch (error) { console.error('AI suggestions failed:', error); return []; } } showSuggestions(suggestions) { this.hideSuggestions(); this.suggestionBox = document.createElement('div'); this.suggestionBox.className = 'ai-suggestions'; this.suggestionBox.innerHTML = ` <div class="ai-suggestions-header"> <span>💡 AI Suggestions</span> <button class="close-btn" onclick="this.parentElement.parentElement.remove()">×</button> </div> <div class="ai-suggestions-list"> ${suggestions.map(suggestion => ` <div class="suggestion-item" onclick="this.dispatchEvent(new CustomEvent('apply-suggestion', {detail: '${suggestion}', bubbles: true}))"> ${suggestion} </div> `).join('')} </div> `; // Add styles const style = document.createElement('style'); style.textContent = ` .ai-suggestions { position: absolute; top: 100%; right: 0; width: 300px; background: white; border: 1px solid #e1e5e9; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 1000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .ai-suggestions-header { padding: 12px 16px; border-bottom: 1px solid #e1e5e9; display: flex; justify-content: space-between; align-items: center; background: #f8f9fa; border-radius: 8px 8px 0 0; font-weight: 600; font-size: 14px; } .close-btn { background: none; border: none; font-size: 18px; cursor: pointer; color: #6c757d; } .ai-suggestions-list { padding: 8px 0; } .suggestion-item { padding: 12px 16px; cursor: pointer; font-size: 14px; line-height: 1.4; color: #495057; } .suggestion-item:hover { background: #f8f9fa; } `; if (!document.head.querySelector('#ai-suggestions-styles')) { style.id = 'ai-suggestions-styles'; document.head.appendChild(style); } // Position relative to editor const editorRect = this.editor.container.getBoundingClientRect(); this.suggestionBox.style.position = 'fixed'; this.suggestionBox.style.top = editorRect.top + 10 + 'px'; this.suggestionBox.style.right = '20px'; document.body.appendChild(this.suggestionBox); // Handle suggestion application this.suggestionBox.addEventListener('apply-suggestion', (e) => { this.applySuggestion(e.detail); }); } applySuggestion(suggestion) { // Insert suggestion as comment or highlight this.editor.insertHTML('<span class="ai-suggestion" title="' + suggestion + '">💡</span>'); this.hideSuggestions(); } hideSuggestions() { if (this.suggestionBox) { this.suggestionBox.remove(); this.suggestionBox = null; } } showTemplateSelector() { const selector = document.createElement('div'); selector.className = 'template-selector'; selector.innerHTML = ` <div class="template-selector-header"> <span>📝 Choose Template</span> <button class="close-btn" onclick="this.parentElement.parentElement.remove()">×</button> </div> <div class="template-list"> ${this.templates.map(template => ` <div class="template-item" data-template-id="${template.id}"> <div class="template-name">${template.name}</div> <div class="template-description">${template.description}</div> <div class="template-category">${template.category}</div> </div> `).join('')} </div> `; // Add styles const style = document.createElement('style'); style.textContent = ` .template-selector { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 500px; max-height: 600px; background: white; border: 1px solid #e1e5e9; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.2); z-index: 1001; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .template-selector-header { padding: 20px 24px; border-bottom: 1px solid #e1e5e9; display: flex; justify-content: space-between; align-items: center; background: #f8f9fa; border-radius: 12px 12px 0 0; font-weight: 600; font-size: 16px; } .template-list { padding: 16px; max-height: 400px; overflow-y: auto; } .template-item { padding: 16px; border: 1px solid #e1e5e9; border-radius: 8px; margin-bottom: 12px; cursor: pointer; transition: all 0.2s; } .template-item:hover { border-color: #007bff; box-shadow: 0 2px 8px rgba(0,123,255,0.1); } .template-name { font-weight: 600; font-size: 16px; color: #212529; margin-bottom: 4px; } .template-description { font-size: 14px; color: #6c757d; margin-bottom: 8px; } .template-category { font-size: 12px; color: #007bff; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; } `; if (!document.head.querySelector('#template-selector-styles')) { style.id = 'template-selector-styles'; document.head.appendChild(style); } document.body.appendChild(selector); // Handle template selection selector.addEventListener('click', (e) => { const templateItem = e.target.closest('.template-item'); if (templateItem) { const templateId = templateItem.getAttribute('data-template-id'); const template = this.templates.find(t => t.id === templateId); if (template) { this.applyTemplate(template); selector.remove(); } } }); // Close on escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { selector.remove(); } }, { once: true }); } applyTemplate(template) { let content = template.content; // Replace variables with placeholders template.variables.forEach(variable => { const placeholder = '[' + variable.toUpperCase() + ']'; content = content.replace(new RegExp('{{' + variable + '}}', 'g'), placeholder); }); this.editor.setContent(content); this.editor.focus(); } async generateContent(prompt) { try { // Simulate AI content generation return new Promise(resolve => { setTimeout(() => { resolve('Generated content based on: "' + prompt + '"'); }, 1000); }); } catch (error) { console.error('AI generation failed:', error); return ''; } } addTemplate(template) { this.templates.push(template); } getTemplates() { return this.templates; } destroy() { var _a; this.hideSuggestions(); // Remove event listeners to prevent memory leaks if ((_a = this.editor) === null || _a === void 0 ? void 0 : _a.editor) { if (this.inputListener) { this.editor.editor.removeEventListener('input', this.inputListener); } if (this.keydownListener) { this.editor.editor.removeEventListener('keydown', this.keydownListener); } } } } class PermissionsSystem { constructor(editor) { this.currentUser = null; this.documentPermissions = new Map(); this.roles = new Map(); this.editor = editor; this.initializeDefaultRoles(); } initializeDefaultRoles() { // Owner role this.roles.set('owner', { id: 'owner', name: 'Owner', permissions: [ { action: 'read', resource: 'document' }, { action: 'write', resource: 'document' }, { action: 'delete', resource: 'document' }, { action: 'share', resource: 'document' }, { action: 'export', resource: 'document' }, { action: 'admin', resource: 'settings' }, { action: 'admin', resource: 'users' } ] }); // Editor role this.roles.set('editor', { id: 'editor', name: 'Editor', permissions: [ { action: 'read', resource: 'document' }, { action: 'write', resource: 'document' }, { action: 'comment', resource: 'comments' }, { action: 'suggest', resource: 'suggestions' }, { action: 'export', resource: 'document' } ] }); // Commenter role this.roles.set('commenter', { id: 'commenter', name: 'Commenter', permissions: [ { action: 'read', resource: 'document' }, { action: 'comment', resource: 'comments' } ] }); // Viewer role this.roles.set('viewer', { id: 'viewer', name: 'Viewer', permissions: [ { action: 'read', resource: 'document' } ] }); } setCurrentUser(user) { this.currentUser = user; this.applyPermissions(); } hasPermission(action, resource) { if (!this.currentUser) return false; const userPermissions = this.currentUser.role.permissions; return userPermissions.some(permission => permission.action === action && permission.resource === resource); } canEdit() { return this.hasPermission('write', 'document'); } canComment() { return this.hasPermission('comment', 'comments'); } canShare() { return this.hasPermission('share', 'document'); } canExport() { return this.hasPermission('export', 'document'); } canDelete() { return this.hasPermission('delete', 'document'); } applyPermissions() { if (!this.currentUser) return; // Apply read-only mode if user can't edit if (!this.canEdit()) { this.editor.setReadOnly(true); } // Hide/show toolbar buttons based on permissions this.updateToolbarVisibility(); // Apply content restrictions this.applyContentRestrictions(); } updateToolbarVisibility() { const toolbar = this.editor.toolbar; if (!toolbar) return; // Hide editing tools if no write permission if (!this.canEdit()) { const editingButtons = toolbar.querySelectorAll('[data-action="bold"], [data-action="italic"], [data-action="underline"]'); editingButtons.forEach((btn) => { btn.style.display = 'none'; }); } // Hide export button if no export permission if (!this.canExport()) { const exportButton = toolbar.querySelector('[data-action="export"]'); if (exportButton) { exportButton.style.display = 'none'; } } } applyContentRestrictions() { var _a; if (!this.canEdit()) { // Disable all editing functionality this.editor.editor.contentEditable = 'false'; // Add visual indicator const indicator = document.createElement('div'); indicator.style.cssText = ` position: absolute; top: 10px; right: 10px; background: #ffc107; color: #000; padding: 4px 8px; border-radius: 4px; font-size: 12px; z-index: 1000; `; indicator.textContent = `${(_a = this.currentUser) === null || _a === void 0 ? void 0 : _a.role.name} - Read Only`; this.editor.container.style.position = 'relative'; this.editor.container.appendChild(indicator); } } shareDocument(userEmail, roleId) { return new Promise((resolve) => { // Simulate API call setTimeout(() => { console.log(`Shared document with ${userEmail} as ${roleId}`); resolve(true); }, 500); }); } revokeAccess(userId) { return new Promise((resolve) => { setTimeout(() => { console.log(`Revoked access for user ${userId}`); resolve(true); }, 500); }); } getAvailableRoles() { return Array.from(this.roles.values()); } createCustomRole(role) { this.roles.set(role.id, role); } // Audit logging logAction(action, details = {}) { var _a, _b; const logEntry = { timestamp: new Date().toISOString(), userId: (_a = this.currentUser) === null || _a === void 0 ? void 0 : _a.id, userName: (_b = this.currentUser) === null || _b === void 0 ? void 0 : _b.name, action, details, documentId: this.editor.options.documentId || 'unknown' }; // In real implementation, send to audit service console.log('Audit Log:', logEntry); // Store locally for demo const logs = JSON.parse(localStorage.getItem('armorEditorAuditLogs') || '[]'); logs.push(logEntry); localStorage.setItem('armorEditorAuditLogs', JSON.stringify(logs.slice(-100))); // Keep last 100 } getAuditLogs(