富文本编辑器-3-基础文本模块

分割线

分割线的功能比较简单
divider.js

import Quill from 'quill'
let BlockEmbed = Quill.import('blots/block/embed')

class Divider extends BlockEmbed { }
Divider.blotName = 'divider'
Divider.tagName = 'hr'

export default Divider

扩展embed类,设置其blotName和tagName(tagName为hr,给hr设置样式)

index.js

import Quill from 'quill'
import Divider from './divider.js'
Quill.register({
  'formats/divider': Divider
})

class ToolbarDivider {
  constructor (quill) {
    this.quill = quill
    this.toolbar = quill.getModule('toolbar')
    if (typeof this.toolbar !== 'undefined') {
      this.toolbar.addHandler('divider', this.insertDivider)
    }
  }
  insertDivider () {
    let range = this.quill.getSelection(true)
    this.quill.insertText(range.index, '\n', Quill.sources.USER)
    this.quill.insertEmbed(range.index + 1, 'divider', true, Quill.sources.USER)
    this.quill.setSelection(range.index + 2, Quill.sources.SILENT)
  }
}
Quill.register('modules/divider', ToolbarDivider)

编写ToolbarDivider类,在toolbar模块添加分割线的handler

格式刷

不需要样式类,只需要handler。实现逻辑为:
第一次点击格式刷,如果选中区域有格式,则保存选中区域格式format,设置按键状态为选中。
用户选中一段文字后,将format作用到选择的文本上,并清空format和重置按键状态。
如果点击格式刷后,再次点击,则取消格式刷功能。

export const copyFormat = (quill, copyFormatting) => {
  // 点击格式刷,如果有选中区域且有样式,则保存其样式,按键状态改为选中。
// 再次点击,删除样式,按键取消选中。
  if (copyFormatting.value === 'un-active') {
    let range = quill.getSelection(true)
    if (range == null || range.length === 0) return
    let format = quill.getFormat(range)
    if (Object.keys(format).length === 0) return
    setCopyFormatting(quill, copyFormatting, 'active', format)
  } else {
    setCopyFormatting(quill, copyFormatting, 'un-active', null)
  }
}
// 设置copyFormatting: 修改保存的样式、按键状态、粘贴样式的处理程序
export const setCopyFormatting = (quill, copyFormatting, value, format) => {
  copyFormatting.value = value
  copyFormatting.format = format
  let toolbar = quill.getModule('toolbar').container
  let brushBtn = toolbar.querySelector('.ql-formatBrush')
  if (value === 'active') {
    brushBtn.classList.add('ql-btn-active')
    quill.on('selection-change', pasteFormatHandler)
  } else {
    brushBtn.classList.remove('ql-btn-active')
    quill.off('selection-change', pasteFormatHandler)
  }
  function pasteFormatHandler (range, oldRange, source) {
    return pasteFormat(range, oldRange, source, quill, copyFormatting)
  }
}
// 粘贴样式的处理程序: 如果选中范围且有保存样式,则粘贴样式,并初始化copyFormatting
export const pasteFormat = (range, oldRange, source, quill, copyFormatting) => {
  if (range && copyFormatting.format) {
    if (range.length === 0) {
    } else {
      quill.formatText(range.index, range.length + 1, copyFormatting.format)
      setCopyFormatting(quill, copyFormatting, 'un-active', null)
    }
  } else {
    // console.log('Cursor not in the editor')
  }
}

index.js没有太多内容,这里不再列出。

段落

Quill默认已有段落的格式,但是实现比较简单,点击Enter键,就重新生成一个<blockquote>标签,由于<blockquote>前后有间隙,就不像一个正常的段落:


效果

html

正常情况下,应该是一个<blockquote>标签,内部多个p标签,这样更合理。那么我们就需要两种样式类:Blockquote, BlockquoteItem。其中 Blockquote是容器,要继承于Container。而BlockquoteItem直接继承Block(Quill blots中默认的p元素),但还需要给BlockquoteItem设置一个className,以和普通的p元素区分开。这是因为Quill默认是用tagName来区分不同的类型的,如果设置了className,则优先以className区分。
Blockquote, BlockquoteItem的代码如下所示:

// import Parchment from 'parchment'
import Quill from 'quill'
let Block = Quill.import('blots/block')
let Container = Quill.import('blots/container')
let Parchment = Quill.import('parchment')

class BlockquoteItem extends Block {
  static formats (domNode) {
    return domNode.tagName === this.tagName ? undefined : super.formats(domNode)
  }

  format (name, value) {
    if (name === Blockquote.blotName && !value) {
      // 设置blockquote: 'false',去掉blockquote样式
      this.replaceWith(Parchment.create(this.statics.scope))
    } else {
      // 设置blockquote: 'blockquote',blockquote样式
      super.format(name, value)
    }
  }

  remove () {
    // 删除及删除父元素
    if (this.prev == null && this.next == null) {
      this.parent.remove()
    } else {
      super.remove()
    }
  }

  replaceWith (name, value) {
    this.parent.isolate(this.offset(this.parent), this.length())
    if (name === this.parent.statics.blotName) {
      // enter添加blockquote-item时,将其放入一个blockquote中
      this.parent.replaceWith(name, value)
      return this
    } else {
      // 点击按键去掉样式时,将父元素展开,该行变成默认的p元素
      this.parent.unwrap()
      return super.replaceWith(name, value)
    }
  }
}
BlockquoteItem.blotName = 'blockquote-item'
BlockquoteItem.tagName = 'p'
BlockquoteItem.className = 'blockquote-item'

class Blockquote extends Container {

  /* 继承container,没有formats,在toolbar.js中无法切换样式 */
  static formats (domNode) {
    return 'blockquote'
  }

  formats () {
    return { [this.statics.blotName]: this.statics.formats(this.domNode) }
  }
  /* 前面插入:如果是BlockquoteItem,直接插入,否则插入到Blockquote外部 */
  insertBefore (blot, ref) {
    if (blot instanceof BlockquoteItem) {
      super.insertBefore(blot, ref)
    } else {
      let index = ref == null ? this.length() : ref.offset(this)
      let after = this.split(index)
      after.parent.insertBefore(blot, after)
    }
  }
  /* 如果下个元素与当前元素一样,则合并 */
  optimize (context) {
    super.optimize(context)
    let next = this.next
    if (next != null && next.prev === this &&
        next.statics.blotName === this.statics.blotName &&
        next.domNode.tagName === this.domNode.tagName) {
      next.moveChildren(this)
      next.remove()
    }
  }
  /* 如果不是一种blot,则将target的内容移动到当前blot中 */
  replace (target) {
    if (target.statics.blotName !== this.statics.blotName) {
      let item = Parchment.create(this.statics.defaultChild)
      target.moveChildren(item)
      this.appendChild(item)
    }
    super.replace(target)
  }
}
Blockquote.blotName = 'blockquote'
Blockquote.scope = Parchment.Scope.BLOCK_BLOT
Blockquote.tagName = 'blockquote'
Blockquote.defaultChild = 'blockquote-item'
Blockquote.allowedChildren = [BlockquoteItem]

export { BlockquoteItem, Blockquote as default }
  1. 设置Blockquote的默认子元素为BlockquoteItem。插入元素时,由于默认target是Block,而不是Blockquote,会调用其replace方法,插入默认子元素BlockquoteItem。
  2. 按enter时,则会调用insertBefore方法继续插入BlockquoteItem。
  3. 再次点击工具栏上的段落图标,则当前行的父元素展开,变为默认的p元素。
  4. 删除子元素时,如果前后都已经没有内容,也会删除段落元素。

index.js没有太多内容,这里不再列出。

撤回和重做

撤回和重做的功能,只需要添加handler就可以实现,所以只有一个index.js

import Quill from 'quill'
let Block = Quill.import('blots/block')
class Redo extends Block {
}
Redo.blotName = 'redo'

class Undo extends Block {
}
Undo.blotName = 'undo'

Quill.register({
  'formats/redo': Redo,
  'formats/undo': Undo
})

class UndoRedo {
  constructor (quill) {
    this.quill = quill
    this.toolbar = quill.getModule('toolbar')
    if (typeof this.toolbar !== 'undefined') {
      this.toolbar.addHandler('redo', this.redo)
      this.toolbar.addHandler('undo', this.undo)
    }
  }
  redo () {
    this.quill.history.redo()
  }
  undo () {
    this.quill.history.undo()
  }
}
Quill.register('modules/undo_redo', UndoRedo)

需要注意的是,需要写上没有实际意义的Redo和Undo类,因为Quill在生成Toolbar时,是根据格式列表来绑定handler的。如果注册这两类,则无法用这种模块化的方式实现redo和undo。当然,如果只是为了实现单一业务,你也可以直接在初始化的时候编写handler:

init () {
       ···
        this.options = {
          modules: {
            toolbar: {
              container: this.$refs.toolbar,
              handlers: {
                'undo': () => { undo(this.quill) },
                'redo': () => { redo(this.quill) },
                 ...
              }
            },
            ...
          },
         ...
      }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 155,770评论 4 358
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,116评论 1 286
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 105,656评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,372评论 0 201
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,704评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,166评论 1 204
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,535评论 2 306
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,259评论 0 193
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,882评论 1 236
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,218评论 2 239
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,760评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,126评论 2 249
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,667评论 3 228
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,935评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,644评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,171评论 2 265
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,119评论 2 258

推荐阅读更多精彩内容

  • 问答题47 /72 常见浏览器兼容性问题与解决方案? 参考答案 (1)浏览器兼容问题一:不同浏览器的标签默认的外补...
    _Yfling阅读 13,624评论 1 92
  • HTML 5 HTML5概述 因特网上的信息是以网页的形式展示给用户的,因此网页是网络信息传递的载体。网页文件是用...
    阿啊阿吖丁阅读 3,345评论 0 0
  • 目前github上已经有将Quill封装一层的vue-quill-editor,对于使用Vue且想直接用Quill...
    minxuan阅读 3,254评论 3 1
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,079评论 18 139
  • 【两面镜子】20180529,D002 碎片时间背唐诗渔歌子,终于知道了斜风细雨念xie,可是今天第一句开...
    我是一只快乐的猪阅读 133评论 0 0