如何通过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
相关产品推荐
相关产品推荐

