技巧-富文本标签插入

1,755 阅读3分钟

一、背景

说到富文本,就像现在在使用的这个富文本一样。这个插入,就像点击工具栏,插入图片或者表格。

业务需求是做一个邮件模板,前端书写某些参数用作占位符,后端将占位符替换成具体的内容。比如:

image.png

其中, ${nameName}就是占位符。那业务上会给出一个参数列表,如 userNameloginUrl等等,在邮件模板中需要填写这些参数。

二、设计思路

需求的设计思路是,用户手动输入占位符。在后续的功能里,当这个邮件模板确定之后,里面的 占位符不能编辑

当用户手动输入一段文字,如 ${username},在编辑器中渲染的其实是 text ,我们很难对 text进行处理。

image.png

而且发现,当用户给这个文字加上颜色时,会自动使用一个 span 标签将其包住,那这样就出现了 text 和 html 两种类型,所以出于能处理的需要,我们强制将这些 占位符 转换成标签

1. 第一种思路

当模板编辑结束,提交的时候,使用正则将形如 ${xxx} 替换成 span

const regex = /\$\{(.*?)\}/g;
editorContent = editorContent.replace(regex, '<span class="highlight">${$1}</span>');

这样在提交到数据库的时候,占位符就是一个span 标签了,另外我们可以给 highlight 设置样式。在数据回显的时候,就可以看到 ${userName} 被包住,并有样式。

image.png

image.png

但是,在用户初次新增的时候,占位符仍是 text , 且看不到颜色,只要用户手动添加颜色才能看到。

另外在后续的调试中发现,如果给占位符添加颜色时,没有包住整个 ${xxxx},那占位符的内容格式将会被打乱。例如:

image.png

image.png

${xxx} 的内容已经被分割开,这样后端无法识别完整的 占位符,哪怕后端或者前端在提交时提示‘参数错误’,用户也不明白参数哪里有问题。

所以核心的一个点,就是不允许用户手动输入 占位符,这就是思路二。

2、第二种思路

插入内容,这就是这篇文章的核心了。我们不允许用户手动输入,或者说用户手动输入的东西不符合前端的规范时,在在前端页面上,就不处理处理。

因此,这就不是单纯的插入文本,也不是大众的插入图片,而是插入 标签 。 这里面涉及到 标签光标,两个知识点。

在使用 vue2-editor 时,虽然是基于 quill 封装的,但并没有把 quill 的众多方法暴露出来,最基本的就很难获取到光标位置和设置光标位置与插入标签。因此,还是使用 vue-quill-editor 来做这个功能。

在业务上的实现是,点击参数列表中的参数时,就将该参数插入到编辑器光标位置。

在技术上的实现是,点击参数时,将参数替换成标签,然后插入到光标位置。

1. 参数预处理

先把参数处理成 ${xxxx}格式

      const param = '${' + value + '}';
2. 获取光标位置
   const selection = quill.selection.savedRange; //含有光标信息
    let index = selection.index; //光标位置
3. 插入方法

vue-quill-editor 的插入方法有以下几种。

  • insertText
quill.insertText(0, 'Hello', 'bold', true);

这是文本的插入,不符合

  • insertEmbed
quill.insertEmbed(10, 'image', 'https://quilljs.com/images/cloud.png');

这个可以插入 对象,应该是可以插入 html, 待定

-dangerouslyPasteHTML

quill.clipboard.dangerouslyPasteHTML(5, '&nbsp;<b>World</b>');

这个可以插入标签,待定

三、占位符处理

经过上面的处理(第二种思路),已经是可以通过点击,向编辑器插入标签了,现在需要对这个标签进行处理,添加颜色,禁止编辑及提示等。

在实际操作中,使用dangerouslyPasteHTML并没有生效。

  quill.clipboard.dangerouslyPasteHTML(index, `<span>${param}</span>`);

因此,转而使用 insertEmbed,其中使用到了 vue-quill-editor的自定义模块。

const Inline = Quill.import('blots/inline');

继承这个模块,自定义创建一个 行盒Node,并对这个 NODE 进行 dom 的操作。

import 'quill/dist/quill.snow.css'; // 引入 Quill
import Quill from 'quill';

const Inline = Quill.import('blots/inline');

class CustomSpanBlot extends Inline {
  static create(value) {
    const node = super.create();
    node.innerHTML = value.text || '';
    node.classList.add('my-span-class'); // 添加自定义类名
    node.style.color = '#5968F6'; //添加颜色
    node.setAttribute('contenteditable', false); //禁止编辑
    return node;
  }

  static formats(node) {
    return node.classList.contains('my-span-class') ? { 'custom-span': true } : undefined;
  }
}

CustomSpanBlot.blotName = 'custom-span'; // 自定义 blot 名称
CustomSpanBlot.tagName = 'span'; // 指定元素标签名

Quill.register(CustomSpanBlot); // 注册自定义 blot

这样这个自定义类就ok了,现在使用这个自定义模块,就可以创建一个 span 标签了。

    insertText(value) {
      const quill = this.$refs.quillEditor.quill;
      const selection = quill.selection.savedRange;
      const param = '${' + value + '}';

      if (selection) {
        let index = selection.index;
        quill.insertEmbed(index, 'custom-span', { text: param });
        quill.setSelection(index + 1);
      }
    },

添加样式

.my-span-class {
  cursor: not-allowed;
  &:hover {
    color: #c8c9cc !important;
  }
}

到目前为止,已经可以实现基本功能了:点击参数,在光标处插入标签,并标签不可编辑。但是还有一些小毛病,如前后间距焦点位置等。

  • 前后间距

现在插入的标签是与前面的内容完全贴合在一起的,为了格式清晰,我们可以在标签前后各插入一个 空格


      if (selection) {
        let index = selection.index;

        quill.insertText(index, ' ');

        quill.insertEmbed(index + 1, 'custom-span', { text: param });
        quill.insertText(index + param.length + 1, ' ');

        quill.setSelection(index + param.length, 0); // 第二个参数设为 0 确保光标不消失
      }
  • 焦点位置

如果你走到了这个地方,你会发现这个焦点始终都不会出现。编辑器插入标签之后,编辑器就失去焦点了,无论怎么设置光标位置,又调用quill.focus()手动添加焦点,都不生效。

经过一步一步的检查,最终发现是 contenteditable属性导致的,只要把这个属性去掉,光标就出现了。但是,我们的目的是让 span 不可被编辑,仅此,不能去掉,这就走进了死胡同。

另外,当点击参数名称插入标签后(此时编辑器无显示光标),我们再次点击参数名称,就会无限插入 标签,这就导致 ${xxx} 的格式全乱了。

image.png

image.png

这都是没有获取到光标引起的。所以,这个两个问题还是很严重的,我们不得不换另一种方案。

四、更换自定义类

之前是使用blots/inline,现在使用 parchment,同样的配置。

import 'quill/dist/quill.snow.css'; // 引入 Quill
import Quill from 'quill';

const Parchment = Quill.import('parchment');
class MyCustomBlot extends Parchment.Embed {
  static blotName = 'customTag';
  static tagName = 'span';
  static className = 'non-editable';

  static create(value) {
    let node = super.create(value);
    node.setAttribute('data-value', value);
    node.setAttribute('contenteditable', false);
    node.style.color = '#5968F6';
    node.innerText = value;
    return node;
  }

  static value(node) {
    return node.getAttribute('data-value');
  }
}

Quill.register(MyCustomBlot);
    insertText(value) {
      const quill = this.$refs.quillEditor.quill;
      const selection = quill.selection.savedRange;
      const param = '${' + value + '}';

      if (selection) {
        let index = selection.index;

        quill.insertText(index, ' '); //前插入空格

        quill.insertEmbed(index + 1, 'customTag', param);

        quill.insertText(index + 2, ' '); //后插入空格

        quill.setSelection(index + 3); //光标位置
      }
    },

至此,这个功能在业务和逻辑都没问题了。

  • 点击参数名称,在光标处插入标签
  • 在标签前后添加空格
  • 给标签设置样式、属性、类名
  • 保证光标正常显示,插入的标签独立,无嵌套
  • 标签不可编辑

五、适应邮箱

通过上面的处理后,占位符 不可编辑的功能已经没问题了,在vue-quill-editor里的内容能正确显示,在数据回显时,样式等等都能正常显示。

但是,通过观察可以发现,富文本里的 居中缩进用的是外联样式来设置text-alignpadding。这样一来,在收件人的网页邮箱里,那是没有这个类名的,这就导致了邮件模板的样式在收件人那里通通失效。因此,我们需要特别处理。

image.png

可以发现,vue-quill-editor在缩进一个单位时,添加了该类名

   .ql-indent-1 {
        padding-left: 3em;
   }

仅此我们需要根据标签的类名,动态地添加上style内联样式。

   addPaddingToClass(htmlString) {
      return htmlString.replace(/(<[^>]*class="[^"]*ql-indent-(\d+)[^"]*"[^>]*)(>)([^<]*)(<\/[^>]*>)/gi, (match, p1, level, p3, p4, p5) => {
        // 计算左内边距
        const padding = parseInt(level, 10) * 3;
        // 如果已经有 style 属性,则追加,否则添加新的 style 属性
        if (p1.includes('style=')) {
          return match.replace(/style="([^"]*)"/, `style="$1; padding-left: ${padding}px;"`);
        } else {
          return `${p1} style="padding-left: ${padding}em;"${p3}${p4}${p5}`;
        }
      });
    },

同理,也要对文本对齐方式进行处理

    addTextAlignStyles(htmlString) {
      // 定义对齐方式的映射
      const alignments = {
        'ql-align-left': 'left',
        'ql-align-center': 'center',
        'ql-align-right': 'right',
        'ql-align-justify': 'justify',
      };

      // 使用正则表达式为不同的类名添加 text-align 样式
      return htmlString.replace(/(<[^>]*class="[^"]*?(ql-align-\w+)[^"]*"[^>]*)(>)([^<]*)(<\/[^>]*>)/gi, (match, p1, className, p3, p4, p5) => {
        const alignment = alignments[className];
        if (alignment) {
          if (p1.includes('style=')) {
            return match.replace(/style="([^"]*)"/, `style="$1 text-align: ${alignment};"`);
          } else {
            return `${p1} style="text-align: ${alignment};"${p3}${p4}${p5}`;
          }
        }
        return match;
      });
    },

到此,邮件模板就基本完成了!