Skip to content

调整工具栏

阿菜 Cai edited this page Nov 8, 2024 · 6 revisions

cherry有五种工具栏位置,如下: cherry工具栏 同时,cherry还默认提供了30+工具栏按钮(都定义在/src/toolbars/hooks/目录下),并且还支持自定义工具栏按钮。

最简单的配置如下:

new Cherry({
    id: 'markdown-container', 
    value: '## hello world', 
    fileUpload: myFileUpload,
    toolbars: {
        // 定义顶部工具栏
        toolbar: ['bold','italic','strikethrough','|','color','header','ruby','|','list','panel','detail'],
        // 定义侧边栏,默认为空
        sidebar: [],
        // 定义顶部右侧工具栏,默认为空
        toolbarRight: [],
        // 定义选中文字时弹出的“悬浮工具栏”,默认为 ['bold', 'italic', 'underline', 'strikethrough', 'sub', 'sup', 'quote', '|', 'size', 'color']
        bubble: false,
        // 定义光标出现在行首位置时出现的“提示工具栏”,默认为 ['h1', 'h2', 'h3', '|', 'checklist', 'quote', 'table', 'code']
        float: false,
    },
});

配置的效果如下图: image

工具栏按钮list

cherry自带的工具栏按钮有以下这些:

  • 辅助类
    • |: 分隔符,单纯的分割工具栏,无任何作用
    • insert: 插入,单纯的占位,点击没有任何效果,用来配置二级菜单
  • 字体样式类
    • bold: 加粗
    • italic: 斜体
    • underline: 下划线
    • strikethrough: 删除线
    • sub: 下标
    • sup: 上标
    • ruby: 实现类似给文字加拼音的效果(当然如何把文字转成拼音需要业务方自行实现,也可参考在线demo用的文字转拼音组件)
    • size: 文字尺寸,自带二级菜单,二级菜单里可选 小、中、大、特大
    • color: 文字颜色,自带二级菜单,二级菜单里可选 文字颜色、文字背景色
  • 段落属性类
    • quote: 引用
    • detail: 手风琴,即可以展开收起内容
    • h1: 一级标题
    • h2: 二级标题
    • h3: 三级标题
    • header: 标题菜单,自带二级菜单,二级菜单里可以选 1~5级标题
    • ul: 无序列表
    • ol: 有序列表
    • checklist: 任务清单
    • list: 列表菜单,自带二级菜单,二级菜单里可选 有序列表、无序列表、任务清单
    • justify: 对齐方式,自带二级菜单,二级菜单里可以选 左对齐、居中、右对齐
    • panel: 信息面板,自带二级菜单,二级菜单里可以选 tips、info、warning、danger、success
  • 插入类
    • image: 插入图片
    • audio: 插入音频
    • video: 插入视频
    • pdf: 插入pdf
    • word: 插入word文档
    • file: 插入普通文件
    • link: 插入链接
    • hr: 插入水平分割线
    • br: 插入新行
    • code: 代码块
    • formula: 插入数学公式
    • toc: 插入目录
    • table: 插入表格
    • drawIo: 插入draw.io画图,点击后会出现draw.io画图面板
    • graph: 插入画图,自带二级菜单,二级菜单里可选 流程图、时序图、状态图、类图、饼图、甘特图
  • 功能类
    • undo: 回撤操作
    • redo: 恢复最近回撤的操作
    • theme: 切换主题,自带二级菜单,主题可配置也可由业务方自行丰富
    • codeTheme: 切换代码块的主题,自带二级菜单
    • mobilePreview: 把预览区域变成h5模式
    • togglePreview: 打开/关闭预览区(用于左右分栏模式,即左边是编辑区域,右边是预览区域)
    • switchModel: 切换编辑/预览模式(用于单栏编辑模式,即点一下是编辑模式,再点一下是预览模式,类似github的交互体验)
    • copy: 复制预览区域的html内容到剪贴板
    • export: 导出,自带二级菜单,二级菜单里可选 导出PDF、导出长图、导出markdown、导出html
    • fullScreen: 全屏/取消全屏
    • settings: 设置,自带二级菜单,二级菜单里可选 常规换行/经典换行切换、关闭/打开预览、隐藏工具栏 (不推荐用了,完全可以自行实现)

自由组装工具栏

我们可以配置将多个工具栏按钮变成某个工具栏的子按钮,从而达到节约空间、按钮分类等目的。

同时我们也可以把字段样式类按钮放进“悬浮工具栏”中,把段落类和插入类按钮放进“提示工具栏”中,把功能类按钮放进侧边栏顶部右侧工具栏

但需要注意,自带二级菜单的按钮无法被放进其他按钮里

代码如下:

new Cherry({
    id: 'markdown-container', 
    value: '## hello world', 
    fileUpload: myFileUpload,
    toolbars: {
        // 定义顶部工具栏
        toolbar: [
            'undo', 'redo', '|',
            // 把字体样式类按钮都放在加粗按钮下面
            {bold:['bold', 'italic', 'underline', 'strikethrough', 'sub', 'sup', 'ruby']},
            'color', 'size', '|', 'header', 'list', 'panel', '|',
            // 把插入类按钮都放在插入按钮下面
            {insert: ['image', 'audio', 'video', 'link', 'hr', 'br', 'code', 'formula', 'toc', 'table', 'drawIo']},
            'graph'
        ],
        // 定义侧边栏,默认为空
        sidebar: ['theme', 'mobilePreview', 'copy'],
        // 定义顶部右侧工具栏,默认为空
        toolbarRight: ['fullScreen', 'export'],
        // 定义选中文字时弹出的“悬浮工具栏”,默认为 ['bold', 'italic', 'underline', 'strikethrough', 'sub', 'sup', 'quote', '|', 'size', 'color']
        bubble: ['bold', 'italic', 'underline', 'strikethrough', 'sub', 'sup', 'ruby', '|', 'color','size',],
        // 定义光标出现在行首位置时出现的“提示工具栏”,默认为 ['h1', 'h2', 'h3', '|', 'checklist', 'quote', 'table', 'code']
        float: ['table', 'code', 'graph'],
    },
});

配置的效果如下图: 字体样式

自定义工具栏按钮

自定义工具栏按钮三步搞定

  1. 创建按钮对象
var customMenu = Cherry.createMenuHook('自定义',  {
    iconName: '', // 声明按钮的图标,空表示不显示图标直接显示文字
});
  1. 在cherry中声明该对象
toolbars: {
    ...
    customMenu: {
        myMenu: customMenu,
    },
}
  1. 将其配置到cherry的工具栏中
toolbars: {
    // 定义顶部工具栏
    toolbar: ['bold', 'myMenu'],
    ...
}

接下来重点介绍第一步,如何创建按钮对象

我们可通过cherry.createMenuHook创建三类按钮对象

第一类普通按钮,可处理点击事件,点击时做相应的操作(比如往编辑区插入内容,或者修改预览区内容等),代码如下:

var customMenuA = Cherry.createMenuHook('加粗斜体',  {
    iconName: 'font', // 声明使用的图标,可选的图标可以在 https://github.com/Tencent/cherry-markdown/tree/main/src/sass/icons 里查看
    onClick: function(selection) {
        return `***${selection}***`;
    }
});

第二类空壳按钮,点击没有任何效果,最主要作用是用来存放子按钮,代码如下:

var customMenuB = Cherry.createMenuHook('实验室',  {
    iconName: '',
});

第三类自带二级菜单的按钮,点击后出现二级菜单,点击二级菜单执行相关操作,代码如下:

var customMenuC = Cherry.createMenuHook('帮助中心',  {
    iconName: 'question',
    subMenuConfig: [
        { noIcon: true, name: '快捷键', onclick: (event)=>{return cherry.insert('快捷键看这里:https://codemirror.net/5/demo/sublime.html');} },
        { noIcon: true, name: '联系我们', onclick: (event)=>{return cherry.insert('我们在这里:https://github.com/Tencent/cherry-markdown');} },
        { noIcon: true, name: '更新日志', onclick: (event)=>{return cherry.insert('我们在这里:https://github.com/Tencent/cherry-markdown/releases');} },
    ]
});

在了解三类按钮后,我们将重点介绍下createMenuHook都提供了哪些工具方法

  1. 获取选中区域的内容
/**
 * 获取用户选中的文本内容,如果没有选中文本,则返回光标所在的位置的内容
 * @param {string} selection 当前选中的文本内容
 * @param {string} type  'line': 当没有选择文本时,获取光标所在行的内容; 'word': 当没有选择文本时,获取光标所在单词的内容
 * @param {boolean} focus true;强行选中光标处的内容,否则只获取选中的内容
 * @returns {string}
 */
getSelection(selection, type = 'word', focus = false)
  1. 让光标选择更多的区域
/**
 * 基于当前已选择区域,获取更多的选择区
 * @param {string} [appendBefore] 选择区前面追加的内容
 * @param {string} [appendAfter] 选择区后面追加的内容
 * @param {function} [cb] 扩大选区后的回调函数,如果cb返回false,则恢复原来的选取
 */
getMoreSelection(appendBefore, appendAfter, cb)
  1. 让光标选择更少的区域
/**
 * 选中除了前后语法后的内容
 * @param {String} lessBefore
 * @param {String} lessAfter
 */
setLessSelection(lessBefore, lessAfter)
  1. 注册点击事件渲染后的回调函数
/**
* 注册点击事件渲染后的回调函数
* @param {function} cb
*/
registerAfterClickCb(cb)
  1. 其他属性
  • isSelections true: 有多个选区/光标;false:只有一个
  • $cherry 实例化的cherry对象
  • editor 编辑区对象,也可通过this.$cherry.editor获得
  • editor.editor 编辑区的codemirror对象,可调用codemirror的api
  • updateMarkdown true:本按钮点击后会变更编辑区内容 false:本按钮点击后不会更新编辑区内容(如复制、导出等按钮)
  • noIcon true: 没有图标,直接显示按钮名 false:尝试显示按钮图标

问题来了,这些方法有什么用

不急,我们先回看下第一类普通按钮的例子:

var customMenuA = Cherry.createMenuHook('加粗斜体',  {
    iconName: 'font', // 声明使用的图标,可选的图标可以在 https://github.com/Tencent/cherry-markdown/tree/main/src/sass/icons 里查看
    onClick: function(selection) {
        return `***${selection}***`;
    }
});

这个例子的实现有三个体验问题

  1. 永远在追加***,用户每点击一次就会追加一层***,这是非常不舒服的
  2. 点击按钮后,光标会选中包含***的内容,这导致用户需要再手动调整光标选区才能修改内容
  3. 如果用户的内容是"hello world",用户如果想加粗"world"这个单词就一定要先选中这个单词才能操作,多少有点麻烦

为解决以上体验问题,我们将上面的例子做了修改,修改后的完整例子如下:

/**
 * 自定义一个自定义菜单按钮
 * 点第一次时,把选中的文字变成同时加粗和斜体
 * 保持光标选区不变,点第二次时,把加粗斜体的文字变成普通文本
 */
var customMenuA = Cherry.createMenuHook('加粗斜体',  {
    iconName: 'font',
    onClick: function(selection) {
        // 获取用户选中的文字,调用getSelection方法后,如果用户没有选中任何文字,会尝试获取光标所在位置的单词或句子
        let $selection = this.getSelection(selection) || '同时加粗斜体';
        
        // 如果是单选,并且选中内容的开始结束内没有加粗斜体语法,则尝试扩大选中范围
        if (!this.isSelections && !/^\s*(\*\*\*)[\s\S]+(\1)/.test($selection)) {
            this.getMoreSelection('***', '***', () => {
                // 调用codemirror的api获取当前选中的文本
                const newSelection = this.editor.editor.getSelection();
                // 判断是否已经是加粗斜体语法了
                const isBoldItalic = /^\s*(\*\*\*)[\s\S]+(\1)/.test(newSelection);
                if (isBoldItalic) {
                    $selection = newSelection;
                }
                return isBoldItalic;
            });
        }

        // 如果选中的文本中已经有加粗语法了,则去掉加粗语法
        if (/^\s*(\*\*\*)[\s\S]+(\1)/.test($selection)) {
            // return $selection.replace(/(^)(\s*)(\*\*\*)([^\n]+)(\3)(\s*)($)/gm, '$1$4$7');  // 这个正则主要是为了支持多行,可以忽略
            return $selection.replace(/^\*\*\*/, '').replace(/\*\*\*$/, '');
        }

        /**
         * 注册缩小选区的规则
         *    注册后,插入“***TEXT***”,选中状态会变成“***【TEXT】***”
         *    如果不注册,插入后选中效果为:“【***TEXT***】”
         */
        this.registerAfterClickCb(() => {
            this.setLessSelection('***', '***');
        });
        // 增加加粗斜体语法
        // return $selection.replace(/(^)([^\n]+)($)/gm, '$1***$2***$3');  // 这个正则主要是为了支持多行,可以忽略
        return `***${$selection}***`;
    }
});

/**
 * 自定义一个空壳自定义按钮
 */
var customMenuB = Cherry.createMenuHook('实验室',  {
    iconName: '',
});

/**
 * 自定义一个带二级菜单的自定义按钮
 */
var customMenuC = Cherry.createMenuHook('帮助中心',  {
    iconName: 'question',
    subMenuConfig: [
        { noIcon: true, name: '快捷键', onclick: (event)=>{return cherry.insert('快捷键看这里:https://codemirror.net/5/demo/sublime.html');} },
        { noIcon: true, name: '联系我们', onclick: (event)=>{return cherry.insert('我们在这里:https://github.com/Tencent/cherry-markdown');} },
        { noIcon: true, name: '更新日志', onclick: (event)=>{return cherry.insert('我们在这里:https://github.com/Tencent/cherry-markdown/releases');} },
    ]
});

new Cherry({
    id: 'markdown-container', 
    value: '## hello world', 
    fileUpload: myFileUpload,
    toolbars: {
        // 定义顶部工具栏
        toolbar: [
            'undo', 'redo', '|',
            // 把字体样式类按钮都放在加粗按钮下面
            {bold:['bold', 'italic', 'underline', 'strikethrough', 'sub', 'sup', 'ruby', 'bold&italic']},
            'color', 'size', '|', 'header', 'list', 'panel', '|',
            // 把插入类按钮都放在插入按钮下面
            {insert: ['image', 'audio', 'video', 'link', 'hr', 'br', 'code', 'formula', 'toc', 'table', 'drawIo']},
            'graph', '|',
            // 把自定义按钮放进来
            {lab: ['drawIo', 'ruby', 'bold&italic']}, 'help'
        ],
        // 定义侧边栏,默认为空
        sidebar: ['theme', 'mobilePreview', 'copy'],
        // 定义顶部右侧工具栏,默认为空
        toolbarRight: ['fullScreen', 'export'],
        // 定义选中文字时弹出的“悬浮工具栏”,默认为 ['bold', 'italic', 'underline', 'strikethrough', 'sub', 'sup', 'quote', '|', 'size', 'color']
        bubble: ['bold', 'italic', 'underline', 'strikethrough', 'sub', 'sup', 'ruby', '|', 'color','size',],
        // 定义光标出现在行首位置时出现的“提示工具栏”,默认为 ['h1', 'h2', 'h3', '|', 'checklist', 'quote', 'table', 'code']
        float: ['table', 'code', 'graph'],
        // 声明自定义按钮
        customMenu: {
            'bold&italic': customMenuA,
            lab: customMenuB,
            help: customMenuC,
        },
    },
});

最终效果如下图: (忽略图片中的加拼音功能,该功能需要额外引入第三方的组件才能实现) 自定义工具栏

自定义工具栏按钮 Icon

现在支持三种方式自定义icon图标,以及支持API的形式动态更新图标(一次性的)

自定义 外部 iconFont 图标

此方式需要准备好字体图标文件,在 src/sass/ch-icon.scss 文件中引用,参照 .ch-icon-*:before { content: "*" }(*代表具体的icon内容) 的方式配置即可

自定义 svg / img 图标

配置项的类型定义在 types/menus.d.ts 中,具体来说形如👇

Type Define 其中 name 就是原来的toolbar名称,不能自定义,除非使用自定义菜单 type 支持 svgimg contenttypesvg 时,填svg文件内容,反之填图片的 url或base64 剩下的就是样式配置,全凭个人喜好了

自定义内部 iconFont 图标

{
  name: 'italic',
  icon: 'bold',
}

这样就可以让下划线使用加粗的图标了,内部图标在 issue #589 里说明了

动态更新图标

另外,每个按钮现在支持通过API的方式更新icon了,如下 API

cherry.toolbar.menus.hooks['header'].updateMenuIcon('bold')

updateMenuIcon 的参数与配置项一致,可参照 自定义 svg / img 图标 部分配置

自定义按钮使用自定义图标的例子

/**
 * 定义一个空壳,用于自行规划cherry已有工具栏的层级结构
 */
var customMenu = Cherry.createMenuHook('实验室',  {
  icon: {
    type: 'svg',
    content: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" /><path d="M8 14s1.5 2 4 2 4-2 4-2" /><line x1="9" y1="9" x2="9.01" y2="9" /><line x1="15" y1="9" x2="15.01" y2="9" /></svg>',
    iconStyle: 'width: 15px; height: 15px; vertical-align: middle;',
  },
});
...
...
new Cherry({toolbars: {
    toolbar: [
      customMenuName: ['ruby', 'audio', 'video'],
    ],
    customMenu: {
      customMenuName: customMenu,
    },
  }});

效果如下: image