基于HTML的Word风格编辑器实现:从零打造功能完备的富文本编辑器

引言

在Web开发中,实现一个功能完备的富文本编辑器是一个常见需求。本文将基于HTML5和JavaScript,结合第三方库,打造一个具有Word风格界面的富文本编辑器,支持格式设置、图片插入、表格创建、文件导入导出等核心功能。

完整代码解析

以下是完整的HTML5富文本编辑器实现代码:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Word 编辑器</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.4.0/mammoth.browser.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background-color: white;
            border-radius: 8px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            padding: 20px;
        }
        .toolbar {
            display: flex;
            flex-wrap: wrap;
            gap: 5px;
            margin-bottom: 15px;
            padding-bottom: 15px;
            border-bottom: 1px solid #eee;
        }
        button, select, input {
            padding: 8px 12px;
            border: 1px solid #ddd;
            border-radius: 4px;
            background-color: white;
            cursor: pointer;
        }
        button:hover {
            background-color: #f0f0f0;
        }
        .editor {
            min-height: 500px;
            border: 1px solid #ddd;
            padding: 20px;
            border-radius: 4px;
            outline: none;
        }
        .file-input {
            display: none;
        }
        .status-bar {
            margin-top: 15px;
            padding-top: 10px;
            border-top: 1px solid #eee;
            color: #666;
            font-size: 14px;
        }
        .active {
            background-color: #e0e0e0;
        }
        .color-picker {
            width: 30px;
            height: 30px;
            padding: 0;
            border: 1px solid #ddd;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Word 编辑器</h1>
        
        <div class="toolbar">
            <button id="bold-btn" title="加粗"><b>B</b></button>
            <button id="italic-btn" title="斜体"><i>I</i></button>
            <button id="underline-btn" title="下划线"><u>U</u></button>
            
            <select id="heading-select">
                <option value="paragraph">正文</option>
                <option value="h1">标题1</option>
                <option value="h2">标题2</option>
                <option value="h3">标题3</option>
            </select>
            
            <select id="font-family">
                <option value="Arial">Arial</option>
                <option value="Times New Roman">Times New Roman</option>
                <option value="Courier New">Courier New</option>
                <option value="宋体">宋体</option>
                <option value="黑体">黑体</option>
                <option value="微软雅黑">微软雅黑</option>
            </select>
            
            <select id="font-size">
                <option value="1">8pt</option>
                <option value="2">10pt</option>
                <option value="3">12pt</option>
                <option value="4">14pt</option>
                <option value="5">18pt</option>
                <option value="6">24pt</option>
                <option value="7">36pt</option>
            </select>
            
            <input type="color" id="text-color" class="color-picker" value="#000000" title="文字颜色">
            <input type="color" id="bg-color" class="color-picker" value="#FFFFFF" title="背景颜色">
            
            <button id="align-left" title="左对齐">左对齐</button>
            <button id="align-center" title="居中对齐">居中</button>
            <button id="align-right" title="右对齐">右对齐</button>
            
            <button id="insert-list" title="插入列表">列表</button>
            <button id="insert-image" title="插入图片">图片</button>
            <button id="insert-link" title="插入链接">链接</button>
            <button id="insert-table" title="插入表格">表格</button>
            
            <button id="undo-btn" title="撤销">撤销</button>
            <button id="redo-btn" title="重做">重做</button>
            
            <button id="import-word" title="导入Word">导入Word</button>
            <button id="export-word" title="导出Word">导出Word</button>
            <button id="export-html" title="导出HTML">导出HTML</button>
            
            <input type="file" id="file-input" class="file-input" accept=".docx">
            <input type="file" id="image-input" class="file-input" accept="image/*" style="display: none;">
        </div>
        
        <div id="editor" class="editor" contenteditable="true"></div>
        
        <div class="status-bar">
            <span id="char-count">0</span> 字符 | <span id="word-count">0</span> 单词 | 光标位置: <span id="cursor-position">0:0</span>
        </div>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', function() {
            const editor = document.getElementById('editor');
            const boldBtn = document.getElementById('bold-btn');
            const italicBtn = document.getElementById('italic-btn');
            const underlineBtn = document.getElementById('underline-btn');
            const headingSelect = document.getElementById('heading-select');
            const fontFamily = document.getElementById('font-family');
            const fontSize = document.getElementById('font-size');
            const textColor = document.getElementById('text-color');
            const bgColor = document.getElementById('bg-color');
            const alignLeft = document.getElementById('align-left');
            const alignCenter = document.getElementById('align-center');
            const alignRight = document.getElementById('align-right');
            const insertList = document.getElementById('insert-list');
            const insertImage = document.getElementById('insert-image');
            const insertLink = document.getElementById('insert-link');
            const insertTable = document.getElementById('insert-table');
            const undoBtn = document.getElementById('undo-btn');
            const redoBtn = document.getElementById('redo-btn');
            const importWord = document.getElementById('import-word');
            const exportWord = document.getElementById('export-word');
            const exportHtml = document.getElementById('export-html');
            const fileInput = document.getElementById('file-input');
            const imageInput = document.getElementById('image-input');
            const charCount = document.getElementById('char-count');
            const wordCount = document.getElementById('word-count');
            const cursorPosition = document.getElementById('cursor-position');
            
            // 初始化编辑器内容
            editor.innerHTML = '<p>开始编辑您的文档...</p>';
            
            // 更新字数统计和光标位置
            function updateCount() {
                const text = editor.innerText;
                charCount.textContent = text.length;
                wordCount.textContent = text.trim() === '' ? 0 : text.trim().split(/\s+/).length;
                
                // 更新光标位置
                const selection = window.getSelection();
                if (selection.rangeCount > 0) {
                    const range = selection.getRangeAt(0);
                    const preCaretRange = range.cloneRange();
                    preCaretRange.selectNodeContents(editor);
                    preCaretRange.setEnd(range.endContainer, range.endOffset);
                    const line = preCaretRange.toString().split('\n').length;
                    const column = range.endOffset;
                    cursorPosition.textContent = `${line}:${column}`;
                }
            }
            
            editor.addEventListener('input', updateCount);
            editor.addEventListener('click', updateCount);
            editor.addEventListener('keyup', updateCount);
            updateCount();
            
            // 加粗
            boldBtn.addEventListener('click', function() {
                document.execCommand('bold', false, null);
                this.classList.toggle('active');
            });
            
            // 斜体
            italicBtn.addEventListener('click', function() {
                document.execCommand('italic', false, null);
                this.classList.toggle('active');
            });
            
            // 下划线
            underlineBtn.addEventListener('click', function() {
                document.execCommand('underline', false, null);
                this.classList.toggle('active');
            });
            
            // 标题样式
            headingSelect.addEventListener('change', function() {
                const value = this.value;
                if (value === 'paragraph') {
                    document.execCommand('formatBlock', false, '<p>');
                } else {
                    document.execCommand('formatBlock', false, `<${value}>`);
                }
            });
            
            // 字体
            fontFamily.addEventListener('change', function() {
                document.execCommand('fontName', false, this.value);
            });
            
            // 字号
            fontSize.addEventListener('change', function() {
                document.execCommand('fontSize', false, this.value);
            });
            
            // 文字颜色
            textColor.addEventListener('input', function() {
                document.execCommand('foreColor', false, this.value);
            });
            
            // 背景颜色
            bgColor.addEventListener('input', function() {
                document.execCommand('hiliteColor', false, this.value);
            });
            
            // 对齐方式
            alignLeft.addEventListener('click', function() {
                document.execCommand('justifyLeft', false, null);
                alignLeft.classList.add('active');
                alignCenter.classList.remove('active');
                alignRight.classList.remove('active');
            });
            
            alignCenter.addEventListener('click', function() {
                document.execCommand('justifyCenter', false, null);
                alignLeft.classList.remove('active');
                alignCenter.classList.add('active');
                alignRight.classList.remove('active');
            });
            
            alignRight.addEventListener('click', function() {
                document.execCommand('justifyRight', false, null);
                alignLeft.classList.remove('active');
                alignCenter.classList.remove('active');
                alignRight.classList.add('active');
            });
            
            // 插入列表
            insertList.addEventListener('click', function() {
                document.execCommand('insertUnorderedList', false, null);
            });
            
            // 插入图片
            insertImage.addEventListener('click', function() {
                const option = prompt('输入图片URL或选择"上传"从本地上传图片', '');
                if (option === '上传') {
                    imageInput.click();
                } else if (option && option !== '') {
                    document.execCommand('insertImage', false, option);
                }
            });
            
            imageInput.addEventListener('change', function(e) {
                const file = e.target.files[0];
                if (!file) return;
                
                const reader = new FileReader();
                reader.onload = function(event) {
                    document.execCommand('insertImage', false, event.target.result);
                };
                reader.readAsDataURL(file);
                this.value = ''; // 重置input,以便可以重复选择同一文件
            });
            
            // 插入链接
            insertLink.addEventListener('click', function() {
                const url = prompt('请输入链接URL:');
                if (url) {
                    const text = window.getSelection().toString() || '链接';
                    document.execCommand('insertHTML', false, `<a href="${url}" target="_blank">${text}</a>`);
                }
            });
            
            // 插入表格
            insertTable.addEventListener('click', function() {
                const rows = prompt('输入行数:', '3');
                const cols = prompt('输入列数:', '3');
                
                if (rows && cols) {
                    let tableHtml = '<table border="1" style="width:100%; border-collapse:collapse;">';
                    for (let i = 0; i < parseInt(rows); i++) {
                        tableHtml += '<tr>';
                        for (let j = 0; j < parseInt(cols); j++) {
                            tableHtml += '<td style="padding:8px;">内容</td>';
                        }
                        tableHtml += '</tr>';
                    }
                    tableHtml += '</table>';
                    
                    document.execCommand('insertHTML', false, tableHtml);
                }
            });
            
            // 撤销
            undoBtn.addEventListener('click', function() {
                document.execCommand('undo', false, null);
                updateCount();
            });
            
            // 重做
            redoBtn.addEventListener('click', function() {
                document.execCommand('redo', false, null);
                updateCount();
            });
            
            // 导入Word
            importWord.addEventListener('click', function() {
                fileInput.click();
            });
            
            fileInput.addEventListener('change', function(e) {
                const file = e.target.files[0];
                if (!file) return;
                
                const reader = new FileReader();
                reader.onload = function(event) {
                    const arrayBuffer = event.target.result;
                    
                    mammoth.extractRawText({arrayBuffer: arrayBuffer})
                        .then(function(result) {
                            editor.innerHTML = result.value;
                            updateCount();
                        })
                        .catch(function(error) {
                            console.error(error);
                            alert('导入Word文件失败: ' + error.message);
                        });
                };
                reader.readAsArrayBuffer(file);
            });
            
            // 导出Word - 使用HTML转DOCX的替代方案
            exportWord.addEventListener('click', function() {
                // 创建一个包含HTML内容的Blob
                const htmlContent = `
                    <!DOCTYPE html>
                    <html>
                    <head>
                        <meta charset="UTF-8">
                        <title>Document</title>
                        <style>
                            body { font-family: Arial, sans-serif; margin: 20px; }
                            h1 { color: #000000; }
                            p { margin-bottom: 10px; }
                        </style>
                    </head>
                    <body>
                        ${editor.innerHTML}
                    </body>
                    </html>
                `;
                
                // 创建一个包含HTML内容的Blob
                const blob = new Blob([htmlContent], { type: 'application/msword' });
                
                // 使用FileSaver.js保存文件
                saveAs(blob, "document.doc");
            });
            
            // 导出HTML
            exportHtml.addEventListener('click', function() {
                const htmlContent = editor.innerHTML;
                const blob = new Blob([htmlContent], { type: 'text/html' });
                saveAs(blob, "document.html");
            });
            
            // 检查当前选区样式
            document.addEventListener('selectionchange', function() {
                const selection = window.getSelection();
                if (selection.rangeCount === 0) return;
                
                const range = selection.getRangeAt(0);
                const parentElement = range.commonAncestorContainer.parentElement;
                
                // 检查加粗
                boldBtn.classList.toggle('active', document.queryCommandState('bold'));
                
                // 检查斜体
                italicBtn.classList.toggle('active', document.queryCommandState('italic'));
                
                // 检查下划线
                underlineBtn.classList.toggle('active', document.queryCommandState('underline'));
                
                // 检查对齐方式
                const align = parentElement.style.textAlign || 
                             window.getComputedStyle(parentElement).textAlign;
                alignLeft.classList.remove('active');
                alignCenter.classList.remove('active');
                alignRight.classList.remove('active');
                
                if (align === 'left') alignLeft.classList.add('active');
                else if (align === 'center') alignCenter.classList.add('active');
                else if (align === 'right') alignRight.classList.add('active');
                
                // 更新光标位置
                updateCount();
            });
            
            // 添加键盘快捷键支持
            document.addEventListener('keydown', function(e) {
                // Ctrl+B - 加粗
                if (e.ctrlKey && e.key === 'b') {
                    e.preventDefault();
                    boldBtn.click();
                }
                // Ctrl+I - 斜体
                else if (e.ctrlKey && e.key === 'i') {
                    e.preventDefault();
                    italicBtn.click();
                }
                // Ctrl+U - 下划线
                else if (e.ctrlKey && e.key === 'u') {
                    e.preventDefault();
                    underlineBtn.click();
                }
                // Ctrl+Z - 撤销
                else if (e.ctrlKey && e.key === 'z') {
                    if (!e.shiftKey) {
                        e.preventDefault();
                        undoBtn.click();
                    }
                }
                // Ctrl+Y 或 Ctrl+Shift+Z - 重做
                else if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'z')) {
                    e.preventDefault();
                    redoBtn.click();
                }
            });
        });
    </script>
</body>
</html>

核心功能实现

1. 编辑器基础结构

编辑器采用经典的contenteditable属性实现可编辑区域:

html 复制代码
<div id="editor" class="editor" contenteditable="true"></div>

配合CSS样式:

css 复制代码
.editor {
    min-height: 500px;
    border: 1px solid #ddd;
    padding: 20px;
    border-radius: 4px;
    outline: none;
}

2. 格式设置功能

使用document.execCommand()实现基础格式设置:

javascript 复制代码
// 加粗
boldBtn.addEventListener('click', function() {
    document.execCommand('bold', false, null);
    this.classList.toggle('active');
});

// 斜体
italicBtn.addEventListener('click', function() {
    document.execCommand('italic', false, null);
    this.classList.toggle('active');
});

// 标题样式
headingSelect.addEventListener('change', function() {
    const value = this.value;
    if (value === 'paragraph') {
        document.execCommand('formatBlock', false, '<p>');
    } else {
        document.execCommand('formatBlock', false, `<${value}>`);
    }
});

3. 样式状态同步

通过selectionchange事件监听选区变化,更新按钮状态:

javascript 复制代码
document.addEventListener('selectionchange', function() {
    // 检查加粗状态
    boldBtn.classList.toggle('active', document.queryCommandState('bold'));
    
    // 检查斜体状态
    italicBtn.classList.toggle('active', document.queryCommandState('italic'));
    
    // 检查对齐方式
    const selection = window.getSelection();
    if (selection.rangeCount > 0) {
        const range = selection.getRangeAt(0);
        const parentElement = range.commonAncestorContainer.parentElement;
        const align = parentElement.style.textAlign || 
                     window.getComputedStyle(parentElement).textAlign;
        
        alignLeft.classList.remove('active');
        alignCenter.classList.remove('active');
        alignRight.classList.remove('active');
        
        if (align === 'left') alignLeft.classList.add('active');
        else if (align === 'center') alignCenter.classList.add('active');
        else if (align === 'right') alignRight.classList.add('active');
    }
});

4. 图片插入功能

支持URL输入和本地文件上传两种方式:

javascript 复制代码
insertImage.addEventListener('click', function() {
    const option = prompt('输入图片URL或选择"上传"从本地上传图片', '');
    if (option === '上传') {
        imageInput.click(); // 触发隐藏的文件输入
    } else if (option && option !== '') {
        document.execCommand('insertImage', false, option);
    }
});

// 本地文件上传处理
imageInput.addEventListener('change', function(e) {
    const file = e.target.files[0];
    if (!file) return;
    
    const reader = new FileReader();
    reader.onload = function(event) {
        document.execCommand('insertImage', false, event.target.result);
    };
    reader.readAsDataURL(file);
    this.value = ''; // 重置input
});

5. 表格插入功能

通过对话框获取行列数,动态生成HTML表格:

javascript 复制代码
insertTable.addEventListener('click', function() {
    const rows = prompt('输入行数:', '3');
    const cols = prompt('输入列数:', '3');
    
    if (rows && cols) {
        let tableHtml = '<table border="1" style="width:100%; border-collapse:collapse;">';
        for (let i = 0; i < parseInt(rows); i++) {
            tableHtml += '<tr>';
            for (let j = 0; j < parseInt(cols); j++) {
                tableHtml += '<td style="padding:8px;">内容</td>';
            }
            tableHtml += '</tr>';
        }
        tableHtml += '</table>';
        
        document.execCommand('insertHTML', false, tableHtml);
    }
});

6. 文件导入导出

导入Word文档

javascript 复制代码
fileInput.addEventListener('change', function(e) {
    const file = e.target.files[0];
    if (!file) return;
    
    const reader = new FileReader();
    reader.onload = function(event) {
        const arrayBuffer = event.target.result;
        
        mammoth.extractRawText({arrayBuffer: arrayBuffer})
            .then(function(result) {
                editor.innerHTML = result.value;
                updateCount();
            })
            .catch(function(error) {
                console.error(error);
                alert('导入Word文件失败: ' + error.message);
            });
    };
    reader.readAsArrayBuffer(file);
});

导出Word文档

javascript 复制代码
exportWord.addEventListener('click', function() {
    const htmlContent = `
        <!DOCTYPE html>
        <html>
        <head>
            <meta charset="UTF-8">
            <title>Document</title>
            <style>
                body { font-family: Arial, sans-serif; margin: 20px; }
                h1 { color: #000000; }
                p { margin-bottom: 10px; }
            </style>
        </head>
        <body>
            ${editor.innerHTML}
        </body>
        </html>
    `;
    
    const blob = new Blob([htmlContent], { type: 'application/msword' });
    saveAs(blob, "document.doc");
});

高级功能实现

1. 键盘快捷键支持

javascript 复制代码
document.addEventListener('keydown', function(e) {
    // Ctrl+B - 加粗
    if (e.ctrlKey && e.key === 'b') {
        e.preventDefault();
        boldBtn.click();
    }
    // Ctrl+I - 斜体
    else if (e.ctrlKey && e.key === 'i') {
        e.preventDefault();
        italicBtn.click();
    }
    // Ctrl+U - 下划线
    else if (e.ctrlKey && e.key === 'u') {
        e.preventDefault();
        underlineBtn.click();
    }
    // Ctrl+Z - 撤销
    else if (e.ctrlKey && e.key === 'z') {
        if (!e.shiftKey) {
            e.preventDefault();
            undoBtn.click();
        }
    }
    // Ctrl+Y 或 Ctrl+Shift+Z - 重做
    else if ((e.ctrlKey && e.key === 'y') || (e.ctrlKey && e.shiftKey && e.key === 'z')) {
        e.preventDefault();
        redoBtn.click();
    }
});

2. 字数统计功能

javascript 复制代码
function updateCount() {
    const text = editor.innerText;
    charCount.textContent = text.length;
    wordCount.textContent = text.trim() === '' ? 0 : text.trim().split(/\s+/).length;
    
    // 更新光标位置
    const selection = window.getSelection();
    if (selection.rangeCount > 0) {
        const range = selection.getRangeAt(0);
        const preCaretRange = range.cloneRange();
        preCaretRange.selectNodeContents(editor);
        preCaretRange.setEnd(range.endContainer, range.endOffset);
        const line = preCaretRange.toString().split('\n').length;
        const column = range.endOffset;
        cursorPosition.textContent = `${line}:${column}`;
    }
}

第三方库使用

  1. Mammoth.js:用于将Word文档(.docx)转换为HTML

    html 复制代码
    <script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.4.0/mammoth.browser.min.js"></script>
  2. FileSaver.js:用于文件保存功能

    html 复制代码
    <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>

改进建议

  1. 样式保留:当前导入Word时只保留了纯文本,建议使用Mammoth的完整转换功能

    javascript 复制代码
    mammoth.convertToHtml({arrayBuffer: arrayBuffer})
        .then(function(result) {
            editor.innerHTML = result.value;
            updateCount();
        })
  2. 撤销重做系统document.execCommand的撤销重做功能有限,建议实现自定义的历史记录栈

  3. 响应式设计:在小屏幕设备上优化工具栏布局

  4. 协作编辑:添加WebSocket支持实现多人协作

总结

本文实现了一个功能完备的富文本编辑器,具有以下特点:

  1. 完整的Word风格界面
  2. 多种格式设置功能
  3. 图片和表格插入
  4. 文件导入导出
  5. 键盘快捷键支持
  6. 字数统计和光标位置显示

这个编辑器可以作为基础框架,根据实际需求进行扩展,如添加Markdown支持、PDF导出、模板功能等。对于生产环境使用,建议考虑使用成熟的编辑器库如Quill、TinyMCE或CKEditor,但理解底层实现原理对于深入掌握前端开发至关重要。

相关推荐
KeithTsui2 小时前
C语言之 比特(bit)、字节(Byte)、字(Word)、整数(Int)
linux·c语言·开发语言·c++·算法·word
GISer_Jing2 小时前
Canvas &SVG &BpmnJS编辑器中Canvas与SVG职能详解
前端·javascript·编辑器
一峰说2 小时前
power BI 倒计时+插件HTML Content,实现更新倒计时看板!
前端·html
DevOpenClub3 小时前
Word 转 HTML API 接口
word
GISer_Jing3 小时前
BPMN.js编辑器设计器与属性面板数据交互
javascript·编辑器·交互
Strive_Sun4 小时前
分析 vscode 运行 node.js 闪退时的场景
ide·vscode·node.js·编辑器
患得患失9495 小时前
【HTML】【面试提问】HTML面试提问总结
前端·html
一块小砖头儿6 小时前
HTML向四周扩散背景
前端·javascript·html
陳長生.6 小时前
JAVA EE(进阶)_HTML
javascript·css·java-ee·html