引言
在Web开发中,实现一个功能完备的富文本编辑器是一个常见需求。本文将基于HTML5和JavaScript,结合第三方库,打造一个具有Word风格界面的富文本编辑器,支持格式设置、图片插入、表格创建、文件导入导出等核心功能。
完整代码解析
以下是完整的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>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
属性实现可编辑区域:
<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, '<p>');
} 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 = '<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文档:
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 = `
<!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. 键盘快捷键支持
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}`;
}
}
第三方库使用
Mammoth.js:用于将Word文档(.docx)转换为HTML
<script src="https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.4.0/mammoth.browser.min.js"></script>
FileSaver.js:用于文件保存功能
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
改进建议
样式保留:当前导入Word时只保留了纯文本,建议使用Mammoth的完整转换功能
mammoth.convertToHtml({arrayBuffer: arrayBuffer}) .then(function(result) { editor.innerHTML = result.value; updateCount(); })
撤销重做系统:
document.execCommand
的撤销重做功能有限,建议实现自定义的历史记录栈响应式设计:在小屏幕设备上优化工具栏布局
协作编辑:添加WebSocket支持实现多人协作
总结
本文实现了一个功能完备的富文本编辑器,具有以下特点:
- 完整的Word风格界面
- 多种格式设置功能
- 图片和表格插入
- 文件导入导出
- 键盘快捷键支持
- 字数统计和光标位置显示
这个编辑器可以作为基础框架,根据实际需求进行扩展,如添加Markdown支持、PDF导出、模板功能等。对于生产环境使用,建议考虑使用成熟的编辑器库如Quill、TinyMCE或CKEditor,但理解底层实现原理对于深入掌握前端开发至关重要。