You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过TinyMCE自定义工具栏按钮切换选中文本的writing-mode属性?

实现自定义工具栏按钮切换文本垂直排版(writing-mode: vertical-rl)

不管是自己写的简单富文本编辑器,还是用主流编辑器框架,核心逻辑都是识别选中的文本、判断是否已应用垂直排版样式、切换样式状态,下面分场景给具体实现方案:

原生JavaScript实现(适合自定义简单编辑器)

先搭基础HTML结构:工具栏按钮加可编辑区域

<div class="toolbar">
  <button id="toggleVertical">垂直排版</button>
</div>
<div contenteditable="true" id="editor">选中这段里的任意文本,点按钮试试切换垂直排版</div>

然后写JS逻辑,绑定按钮点击事件,处理选区和样式切换:

const toggleBtn = document.getElementById('toggleVertical');
const editor = document.getElementById('editor');

toggleBtn.addEventListener('click', () => {
  // 获取当前文本选区
  const selection = window.getSelection();
  if (!selection.rangeCount) return; // 没选中文本直接返回

  const range = selection.getRangeAt(0);
  const selectedText = range.toString().trim();
  if (!selectedText) return;

  // 向上遍历选区的祖先节点,检查是否已经有vertical-rl样式的元素
  let verticalWrap = null;
  let currentNode = range.commonAncestorContainer;
  while (currentNode !== editor) {
    if (currentNode.style?.writingMode === 'vertical-rl') {
      verticalWrap = currentNode;
      break;
    }
    currentNode = currentNode.parentNode;
  }

  if (verticalWrap) {
    // 已经有垂直排版,移除包裹元素,恢复原文本结构
    const parent = verticalWrap.parentNode;
    while (verticalWrap.firstChild) {
      parent.insertBefore(verticalWrap.firstChild, verticalWrap);
    }
    parent.removeChild(verticalWrap);
  } else {
    // 还没应用,用span包裹选中文本并设置样式
    const span = document.createElement('span');
    span.style.writingMode = 'vertical-rl';
    span.style.display = 'inline-block'; // 确保垂直排版生效(inline元素可能不支持)
    try {
      range.surroundContents(span);
    } catch (e) {
      // 跨多个元素选中时surroundContents会报错,这里可以做兼容处理,比如拆分选区
      console.warn('选区跨元素,无法直接包裹,建议选中单个块内的文本');
    }
  }

  // 恢复选区,避免点击按钮后选区丢失
  selection.removeAllRanges();
  selection.addRange(range);
});

注意点

  • surroundContents只适合选区在单个元素内的情况,如果选中了跨多个段落/元素的文本,会抛出错误,需要额外处理复杂选区
  • 给span加display: inline-block是因为writing-mode在纯inline元素上可能不生效

Quill编辑器实现

Quill需要先注册自定义格式,再绑定工具栏按钮:

// 注册自定义垂直排版格式
Quill.register({
  'formats/verticalText': class VerticalText {
    // 创建应用格式的DOM元素
    static create() {
      const span = document.createElement('span');
      span.style.writingMode = 'vertical-rl';
      span.style.display = 'inline-block';
      return span;
    }

    // 判断节点是否应用了该格式
    static formats(node) {
      return node.style.writingMode === 'vertical-rl' ? true : false;
    }

    // 切换格式的逻辑
    static toggle(range) {
      const quill = this.quill;
      const hasFormat = quill.getFormat(range).verticalText;
      if (hasFormat) {
        quill.removeFormat(range);
      } else {
        quill.format('verticalText', true, Quill.sources.USER);
      }
    }
  }
}, true);

// 初始化Quill并添加工具栏按钮
const quill = new Quill('#editor', {
  theme: 'snow',
  toolbar: [['bold', 'italic'], ['verticalText']] // 把自定义按钮加入工具栏
});

// 给工具栏按钮绑定事件
const toolbar = quill.getModule('toolbar');
toolbar.addHandler('verticalText', function() {
  const range = quill.getSelection();
  if (range) {
    quill.format('verticalText', !quill.getFormat(range).verticalText);
  }
});

CKEditor 5实现

CKEditor 5需要写自定义插件,注册命令和工具栏按钮:

import { Plugin } from '@ckeditor/ckeditor5-core';
import { ButtonView } from '@ckeditor/ckeditor5-ui';

// 自定义垂直排版插件
class VerticalTextPlugin extends Plugin {
  init() {
    const editor = this.editor;

    // 注册切换命令
    editor.commands.add('toggleVerticalText', {
      execute() {
        const model = editor.model;
        const selection = model.document.selection;

        model.change(writer => {
          if (selection.hasAttribute('verticalText')) {
            // 移除格式属性
            selection.removeAttribute('verticalText');
            selection.getFirstRange().getItems().forEach(item => {
              if (item.hasAttribute('verticalText')) {
                writer.removeAttribute('verticalText', item);
              }
            });
          } else {
            // 添加格式属性
            writer.setAttribute('verticalText', true, selection);
          }
        });
      },

      // 刷新按钮状态(是否激活)
      refresh() {
        const model = this.editor.model;
        const selection = model.document.selection;
        this.isEnabled = model.schema.checkChild(selection.focus.parent, '$text');
        this.value = selection.hasAttribute('verticalText');
      }
    });

    // 添加工具栏按钮
    editor.ui.componentFactory.add('verticalText', locale => {
      const button = new ButtonView(locale);
      button.set({
        label: '垂直排版',
        withText: true,
        tooltip: true
      });
      // 绑定按钮激活状态到命令
      button.bind('isOn').to(editor.commands.get('toggleVerticalText'), 'value');
      // 点击执行命令
      button.on('execute', () => {
        editor.execute('toggleVerticalText');
        editor.focus();
      });
      return button;
    });

    // 配置模型到视图的转换(把模型属性转成DOM样式)
    editor.conversion.for('downcast').attributeToElement({
      model: 'verticalText',
      view: () => {
        return editor.conversion.writer.createAttributeElement('span', {
          style: 'writing-mode: vertical-rl; display: inline-block;'
        });
      }
    });

    // 配置视图到模型的转换(识别DOM样式转成模型属性)
    editor.conversion.for('upcast').elementToAttribute({
      view: {
        name: 'span',
        styles: { 'writing-mode': 'vertical-rl' }
      },
      model: 'verticalText'
    });
  }
}

// 初始化编辑器时引入插件
ClassicEditor
  .create(document.querySelector('#editor'), {
    plugins: [VerticalTextPlugin, 'Essentials', 'Bold', 'Italic'], // 加入自定义插件
    toolbar: ['bold', 'italic', 'verticalText'] // 工具栏添加按钮
  })
  .catch(err => console.error(err));

内容的提问来源于stack exchange,提问作者Andrew Foulds

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 04:17:13