Summernote图片预览功能:模态框与缩略图实现
【免费下载链接】summernote Super simple WYSIWYG editor 项目地址: https://gitcode.com/gh_mirrors/su/summernote
引言:编辑器图片预览的用户体验痛点
在富文本编辑(Rich Text Editing)场景中,图片上传与预览功能直接影响内容创作者的工作效率。传统编辑器常存在三大痛点:上传前缺乏视觉确认导致错误提交、模态框(Modal Dialog)交互卡顿影响流畅度、缩略图(Thumbnail)样式不一致破坏界面统一性。Summernote作为轻量级所见即所得(WYSIWYG)编辑器,通过模块化设计提供了可定制的图片预览解决方案,本文将从实现原理到高级扩展全面解析其技术细节。
核心组件解析:ImageDialog模块架构
Summernote的图片预览功能核心实现位于ImageDialog.js模块,采用MVC(Model-View-Controller)设计模式组织代码。该模块通过以下关键组件协同工作:
1. 类结构与初始化流程
export default class ImageDialog {
constructor(context) {
this.context = context; // 编辑器上下文对象
this.ui = $.summernote.ui; // UI组件生成器
this.options = context.options; // 编辑器配置项
this.lang = this.options.langInfo; // 多语言支持
}
initialize() {
// 创建模态框DOM结构
const body = [
'<div class="form-group note-form-group note-group-select-from-files">',
'<label class="note-form-label">' + this.lang.image.selectFromFiles + '</label>',
'<input type="file" accept="'+this.options.acceptImageFileTypes+'" multiple/>',
this.createSizeLimitationHtml(), // 文件大小限制提示
'</div>',
'<div class="form-group note-group-image-url">',
'<label>' + this.lang.image.url + '</label>',
'<input type="text" class="note-image-url form-control"/>',
'</div>'
].join('');
this.$dialog = this.ui.dialog({
title: this.lang.image.insert,
body: body,
footer: '<button class="note-image-btn btn btn-primary">'+this.lang.image.insert+'</button>'
}).render();
}
}
关键技术点:
- 上下文对象(
context)封装了编辑器核心API,实现模块间解耦 - UI工厂(
$.summernote.ui)提供标准化组件生成,确保跨主题一致性 - 多语言支持(
langInfo)通过语言文件(如summernote-zh-CN.js)实现国际化适配
2. 模态框交互逻辑
模态框的显示与隐藏通过show()方法触发,内部采用Promise机制处理异步交互:
show() {
this.context.invoke('editor.saveRange'); // 保存光标位置
this.showImageDialog().then((data) => {
this.ui.hideDialog(this.$dialog); // 关闭对话框
this.context.invoke('editor.restoreRange'); // 恢复光标位置
if (typeof data === 'string') {
this.context.invoke('editor.insertImage', data); // 插入URL图片
} else {
this.context.invoke('editor.insertImagesOrCallback', data); // 插入文件图片
}
}).fail(() => {
this.context.invoke('editor.restoreRange'); // 失败时恢复状态
});
}
交互时序图:
缩略图预览实现:File API与内存URL技术
尽管基础版ImageDialog未直接实现缩略图预览,但可通过HTML5 File API扩展实现该功能。以下是基于Summernote架构的实现方案:
1. 文件选择事件处理
在initialize()方法中为文件输入框绑定change事件,通过FileReader接口生成预览图:
// 在ImageDialog.initialize()中添加
this.$imageInput = this.$dialog.find('input[type="file"]');
this.$imageInput.on('change', (e) => {
const files = e.target.files;
if (files && files.length > 0) {
this.previewThumbnails(files); // 生成缩略图预览
}
});
2. 缩略图生成与渲染
实现previewThumbnails方法处理文件预览逻辑:
previewThumbnails(files) {
const $previewContainer = this.createPreviewContainer();
Array.from(files).forEach(file => {
if (!this.isValidImageFile(file)) return;
const reader = new FileReader();
reader.onload = (e) => {
const $thumbnail = this.createThumbnailElement(file, e.target.result);
$previewContainer.append($thumbnail);
};
reader.readAsDataURL(file); // 将文件转换为DataURL
});
}
createPreviewContainer() {
// 创建预览容器DOM
const $container = $('<div class="note-image-preview-container"></div>');
this.$dialog.find('.note-group-select-from-files').append($container);
return $container;
}
createThumbnailElement(file, dataUrl) {
// 创建单个缩略图元素
return $(`
<div class="note-image-thumbnail">
<img src="${dataUrl}" alt="${file.name}" class="preview-img"/>
<div class="thumbnail-info">
<span class="file-name">${file.name}</span>
<span class="file-size">${this.formatFileSize(file.size)}</span>
</div>
<button class="thumbnail-remove">×</button>
</div>
`).on('click', '.thumbnail-remove', function() {
$(this).parent().remove();
});
}
技术亮点:
- 使用
readAsDataURL将文件转换为Base64编码的内存URL,避免临时文件存储 - 采用事件委托机制处理缩略图删除按钮点击事件,优化性能
- 文件名与大小显示增强用户对上传内容的掌控感
3. 样式适配与响应式设计
添加以下CSS确保缩略图在不同主题(BS3/BS4/BS5)下的一致性:
.note-image-preview-container {
margin-top: 1rem;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
max-height: 200px;
overflow-y: auto;
padding: 0.5rem;
border: 1px dashed #ced4da;
border-radius: 0.25rem;
}
.note-image-thumbnail {
position: relative;
width: 100px;
height: 100px;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
overflow: hidden;
.preview-img {
width: 100%;
height: 100%;
object-fit: cover; // 保持比例填充
}
.thumbnail-info {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 0.25rem;
background: rgba(0,0,0,0.6);
color: white;
font-size: 0.75rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.thumbnail-remove {
position: absolute;
top: 0;
right: 0;
width: 20px;
height: 20px;
background: rgba(255,255,255,0.5);
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
}
响应式行为:
- 使用Flex布局实现缩略图自动换行
max-height与overflow-y: auto防止大量图片导致的界面溢出object-fit: cover确保图片在缩略图容器中保持正确比例
高级扩展:从基础预览到功能增强
1. 图片尺寸限制与验证
在上传前添加文件大小与类型验证,避免无效提交:
isValidImageFile(file) {
// 验证文件类型
if (!this.options.acceptImageFileTypes.test(file.type)) {
this.showError(this.lang.image.invalidType);
return false;
}
// 验证文件大小
if (this.options.maximumImageFileSize &&
file.size > this.options.maximumImageFileSize) {
const maxSize = this.formatFileSize(this.options.maximumImageFileSize);
this.showError(`${this.lang.image.maximumFileSize}: ${maxSize}`);
return false;
}
return true;
}
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
2. 多图上传与预览队列
修改previewThumbnails方法支持多图预览队列管理:
previewThumbnails(files) {
const $container = this.createPreviewContainer();
const previewPromises = Array.from(files).map(file =>
new Promise((resolve) => {
if (!this.isValidImageFile(file)) {
resolve(null);
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const $thumbnail = this.createThumbnailElement(file, e.target.result);
resolve($thumbnail);
};
reader.readAsDataURL(file);
})
);
Promise.all(previewPromises).then($thumbnails => {
$thumbnails.filter(Boolean).forEach($t => $container.append($t));
});
}
队列管理优势:
- 异步处理多个文件预览生成,避免UI阻塞
- Promise.all确保所有预览图加载完成后统一渲染
- 过滤无效文件,保持预览区整洁
性能优化:大型图片处理策略
当处理高分辨率图片时,直接生成原图预览可能导致内存占用过高和UI卡顿。可通过以下方法优化:
1. 图片压缩与尺寸调整
使用Canvas API对大图进行压缩处理:
compressImage(file, maxWidth = 1200, quality = 0.8) {
return new Promise((resolve) => {
const img = new Image();
img.src = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(img.src); // 释放内存URL
// 计算压缩后的尺寸
let width = img.width;
let height = img.height;
if (width > maxWidth) {
height *= maxWidth / width;
width = maxWidth;
}
// 创建Canvas并绘制压缩图
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
// 转换为Blob对象
canvas.toBlob(
(blob) => resolve(blob),
'image/jpeg',
quality
);
};
});
}
2. 延迟加载与虚拟滚动
对于超过10张的图片预览列表,实现虚拟滚动(Virtual Scrolling):
.note-image-preview-container {
position: relative;
height: 200px;
overflow: hidden;
}
.thumbnail-virtual-list {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow-y: auto;
}
// 虚拟滚动实现核心逻辑
initVirtualScroll($container) {
const itemHeight = 100; // 固定高度
const containerHeight = $container.height();
const visibleCount = Math.ceil(containerHeight / itemHeight);
$container.on('scroll', () => {
const scrollTop = $container.scrollTop();
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = startIndex + visibleCount;
this.renderVisibleItems(startIndex, endIndex);
});
}
浏览器兼容性处理
| 特性 | Chrome | Firefox | Safari | Edge | IE11 |
|---|---|---|---|---|---|
| FileReader API | ✅ 7+ | ✅ 3.6+ | ✅ 6+ | ✅ 12+ | ✅ 10+ |
| Canvas压缩 | ✅ 10+ | ✅ 16+ | ✅ 6+ | ✅ 12+ | ❌ 不支持 |
| Promise | ✅ 32+ | ✅ 29+ | ✅ 8+ | ✅ 12+ | ❌ 需要polyfill |
兼容处理策略:
- 对IE11等老旧浏览器,降级为无预览基础上传模式
- 使用
core-js提供Promise polyfill - 通过
env.isSupportTouch等环境检测API适配移动设备
总结与扩展方向
Summernote的图片预览功能通过模块化设计提供了坚实基础,开发者可根据需求扩展以下高级特性:
- 拖放上传:监听
dragover/drop事件实现拖放预览 - 图片编辑:集成Cropper.js实现裁剪、旋转等编辑功能
- 云存储集成:对接七牛云、阿里云等OSS实现直传
- 预览图缓存:使用IndexedDB缓存历史预览图加速加载
【免费下载链接】summernote Super simple WYSIWYG editor 项目地址: https://gitcode.com/gh_mirrors/su/summernote
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



