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
JavaScript
'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(