Summernote图片预览功能:模态框与缩略图实现

Summernote图片预览功能:模态框与缩略图实现

【免费下载链接】summernote Super simple WYSIWYG editor 【免费下载链接】summernote 项目地址: 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'); // 失败时恢复状态
  });
}

交互时序图

mermaid

缩略图预览实现: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">&times;</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-heightoverflow-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);
  });
}

浏览器兼容性处理

特性ChromeFirefoxSafariEdgeIE11
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的图片预览功能通过模块化设计提供了坚实基础,开发者可根据需求扩展以下高级特性:

  1. 拖放上传:监听dragover/drop事件实现拖放预览
  2. 图片编辑:集成Cropper.js实现裁剪、旋转等编辑功能
  3. 云存储集成:对接七牛云、阿里云等OSS实现直传
  4. 预览图缓存:使用IndexedDB缓存历史预览图加速加载

【免费下载链接】summernote Super simple WYSIWYG editor 【免费下载链接】summernote 项目地址: https://gitcode.com/gh_mirrors/su/summernote

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值