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

引言

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

完整代码解析

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

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>Bb>button>
            <button id="italic-btn" title="斜体"><i>Ii>button>
            <button id="underline-btn" title="下划线"><u>Uu>button>
            
            <select id="heading-select">
                <option value="paragraph">正文option>
                <option value="h1">标题1option>
                <option value="h2">标题2option>
                <option value="h3">标题3option>
            select>
            
            <select id="font-family">
                <option value="Arial">Arialoption>
                <option value="Times New Roman">Times New Romanoption>
                <option value="Courier New">Courier Newoption>
                <option value="宋体">宋体option>
                <option value="黑体">黑体option>
                <option value="微软雅黑">微软雅黑option>
            select>
            
            <select id="font-size">
                <option value="1">8ptoption>
                <option value="2">10ptoption>
                <option value="3">12ptoption>
                <option value="4">14ptoption>
                <option value="5">18ptoption>
                <option value="6">24ptoption>
                <option value="7">36ptoption>
            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">导入Wordbutton>
            <button id="export-word" title="导出Word">导出Wordbutton>
            <button id="export-html" title="导出HTML">导出HTMLbutton>
            
            <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">0span> 字符 | <span id="word-count">0span> 单词 | 光标位置: <span id="cursor-position">0:0span>
        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 = '

开始编辑您的文档...

'
; // 更新字数统计和光标位置 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, '

'); } 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, `${url}" target="_blank">${text}`); } }); // 插入表格 insertTable.addEventListener('click', function() { const rows = prompt('输入行数:', '3'); const cols = prompt('输入列数:', '3'); if (rows && cols) { let tableHtml = '

';for(let i =0; i <parseInt(rows); i++){ tableHtml +='';for(let j =0; j <parseInt(cols); j++){ tableHtml +='';} tableHtml +='';} tableHtml +='
内容
'
; 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 = ` Document ${editor.innerHTML} `; // 创建一个包含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属性实现可编辑区域:

<div id="editor" class="editor" contenteditable="true">div>

配合CSS样式:

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

2. 格式设置功能

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

// 加粗
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, '

'); } else { document.execCommand('formatBlock', false, `<${value}>`); } });

3. 样式状态同步

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

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输入和本地文件上传两种方式:

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表格:

insertTable.addEventListener('click', function() {
    const rows = prompt('输入行数:', '3');
    const cols = prompt('输入列数:', '3');
    
    if (rows && cols) {
        let tableHtml = '';for(let i =0; i <parseInt(rows); i++){
            tableHtml +='';for(let j =0; j <parseInt(cols); j++){
                tableHtml +='';}
            tableHtml +='';}
        tableHtml +='
内容
'
; document.execCommand('insertHTML', false, tableHtml); } });

6. 文件导入导出

导入Word文档

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文档

exportWord.addEventListener('click', function() {
    const htmlContent = `
        
        
        
            
            Document
            
        
        
            ${editor.innerHTML}
        
        
    `;
    
    const blob = new Blob([htmlContent], { type: 'application/msword' });
    saveAs(blob, "document.doc");
});

高级功能实现

1. 键盘快捷键支持

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. 字数统计功能

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

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

    <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js">script>
    

改进建议

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

    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,但理解底层实现原理对于深入掌握前端开发至关重要。

你可能感兴趣的:(html,word,编辑器)