Tinymce - 宇宙第一富文字編輯器?[3]

嘉禾生2310發表於2019-09-20

自定義外掛

Tinymce官方提供了豐富的外掛,足以滿足關於一般富文字的需求,也可以通過進一步的配置,讓其更符合實際的業務場景。但是,如果這還滿足不了你的需求,想進一步的定製化呢?

Tinymce提供了豐富的介面,用以編寫自定義的外掛。並且,官方的外掛原始碼也非常清晰易懂,在寫自己的外掛的時候,可以做一個詳細的參考。

編寫外掛

其實編寫一個外掛很簡單,只需要做3件事情。

  1. 製作一個Svg格式的圖示,這個圖示用於在工具欄(toolbar)的展示。(按鈕長什麼樣)
  2. 給工具欄新增一個按鈕,並給這個按鈕設定點選、下拉等要觸發的事件。(按鈕可以做什麼)
  3. 註冊一個命令,對按鈕的操作可以觸發這個命令,命令的回撥方法裡對編輯器內容進行操作。(具體怎麼做)

行高外掛

因為公司這邊的業務要求時,開發一個功能與微信公眾號文章編輯器基本功能對稱的富文字編輯器。而微信編輯器有一個設定行高的功能,Tinymce官方沒有提供,雖然可以通過配置格式化樣式進行簡單的開發,但為了統一體驗,就仿照微信編輯器在工具欄新增了一個可以設定行距的按鈕。

import Tinymce from 'tinymce'

Tinymce.PluginManager.add('lineheight', function (editor) {
  // 執行方法
  const actionFunction = function (editor, val) {
    const value = val || editor.getParam('lineheight_default_value', 1.5)
    editor.formatter.apply('lineheight', { value })
  }

  // 命令
  editor.addCommand('mceLineHeight', function (ui, value) {
    actionFunction(editor, value)
  })

  // toolbar 按鈕 圖示
  editor.ui.registry.addIcon('lineheight', `
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
      <path d="M5,5 L19,5 L19,7 L5,7 L5,5 Z M13,9 L19,9 L19,11 L13,11 L13,9 Z M13,13 L19,13 L19,15 L13,15 L13,13 Z M5,17 L19,17 L19,19 L5,19 L5,17 Z M11.07,11.46 L8.91,9.84 L6.75,11.46 L5.79,10.18 L8.43,8.2 C8.71444444,7.98666667 9.10555556,7.98666667 9.39,8.2 L12.03,10.18 L11.07,11.46 Z M8.91,14.16 L11.07,12.54 L12.03,13.82 L9.39,15.8 C9.10555556,16.0133333 8.71444444,16.0133333 8.43,15.8 L5.79,13.82 L6.75,12.54 L8.91,14.16 Z" id="行間距"></path>
    </svg>
  `)

  // toolbar 按鈕功能
  editor.ui.registry.addSplitButton('lineheight', {
    tooltip: '行間距',
    // 圖示
    icon: 'lineheight',
    // 初始化
    onSetup (api) {
      editor.formatter.register({
        lineheight: {
          selector: 'p,h1,h2,h3,h4,h5,h6,table,td,th,div,ul,ol,li,section,article,header,footer,figcaption',
          styles: { 'line-height': '%value' }
        }
      })
    },
    // 圖示點選
    onAction (api) {
      return editor.execCommand('mceLineHeight')
    },
    // 列表項點選
    onItemAction (buttonApi, value) {
      return editor.execCommand('mceLineHeight', false, value)
    },
    // 初始化列表
    fetch (callback) {
      const items = [
        {
          type: 'choiceitem',
          text: '1',
          value: 1
        },
        {
          type: 'choiceitem',
          text: '1.5',
          value: 1.5
        },
        {
          type: 'choiceitem',
          text: '1.75',
          value: 1.75
        },
        {
          type: 'choiceitem',
          text: '2',
          value: 2
        },
        {
          type: 'choiceitem',
          text: '3',
          value: 3
        },
        {
          type: 'choiceitem',
          text: '4',
          value: 4
        },
        {
          type: 'choiceitem',
          text: '5',
          value: 5
        }
      ]
      callback(items)
    }
  })
})
複製程式碼

這個外掛只是對當前內容的一種格式化。比較簡單。

引入時機

外掛一定要在tinymce引入之後,例項初始化之前引入。如果外掛出現重名,會優先使用自己專案中的外掛。換言之,初始化的時候,會先找已註冊的外掛,沒有找到的話,會根據配置,去相對路徑取外掛檔案。

import Tinymce from 'tinymce'
import './plugins/lineheight'

Tinymce.init({
    selector: '.textarea',
    plugins: ['lineheight']
})

複製程式碼

預覽外掛

因為編輯器自帶的外掛不能滿足業務需求,所以自己寫了一個預覽外掛。

import Tinymce from 'tinymce'

Tinymce.PluginManager.add('preview', function (editor) {
  const adaptStyle = `
    body {
      width: 375px !important;
      height: 667px !important;
      overflow-x: hidden !important;
      overflow-y: auto !important;
      margin: 0 !important;
      padding: 0 !important;
      font-size: 17px;
    }
    .adaptive-screen-width {
      width: 100% !important;
      height: auto;
    }
  
  `

  const Settings = {
    getContentStyle (editor) {
      return editor.getParam('content_style', '')
    },
    shouldUseContentCssCors (editor) {
      return editor.getParam('content_css_cors', false, 'boolean')
    },
    getBodyId (editor) {
      let bodyId = editor.settings.body_id || 'tinymce'
      if (bodyId.indexOf('=') !== -1) {
        bodyId = editor.getParam('body_id', '', 'hash')
        bodyId = bodyId[editor.id] || bodyId
      }
      return bodyId
    },
    getBodyClass (editor) {
      let bodyClass = editor.settings.body_class || ''
      if (bodyClass.indexOf('=') !== -1) {
        bodyClass = editor.getParam('body_class', '', 'hash')
        bodyClass = bodyClass[editor.id] || ''
      }
      return bodyClass
    },
    getDirAttr (editor) {
      const encode = editor.dom.encode
      let directionality = editor.getBody().dir
      return directionality ? ' dir="' + encode(directionality) + '"' : ''
    }
  }

  const getPreviewFrame = function (editor) {
    // <head>
    let headHtml = ''
    const encode = editor.dom.encode
    const contentStyle = Settings.getContentStyle(editor)
    headHtml += `<base href="${encode(editor.documentBaseURI.getURI())}">`
    if (contentStyle) {
      headHtml += `<style type="text/css">${contentStyle + adaptStyle}</style>`
    }
    const cors = Settings.shouldUseContentCssCors(editor) ? ' crossorigin="anonymous"' : ''
    Array.from(editor.contentCSS).forEach(url => {
      headHtml += `<link type="text/css" rel="stylesheet" href="${encode(editor.documentBaseURI.toAbsolute(url))}" ${cors}>`
    })
    // <body>
    const bodyId = Settings.getBodyId(editor)
    const bodyClass = Settings.getBodyClass(editor)
    const dirAttr = Settings.getDirAttr(editor)
    // 禁用點選事件
    const preventClicksOnLinksScript = '<script>document.addEventListener && document.addEventListener("click", function(e) {for (var elm = e.target; elm; elm = elm.parentNode) {if (elm.nodeName === "A") {e.preventDefault();}}}, false);</script> '
    // html
    const html = `
      <!DOCTYPE html>
        <html lang="zh_cn">
          <head>${headHtml}</head>
          <body id="${encode(bodyId)}" class="mce-content-body ${encode(bodyClass)}" ${dirAttr}>
            ${editor.getContent()}
            ${preventClicksOnLinksScript}
          </body>
        </html>  
    `
    // iframe
    return `
      <iframe sandbox="allow-scripts allow-same-origin" frameborder="0" width="395" height="667" srcdoc="${encode(html)}">
      </iframe>
    `
  }

  const getPreviewDialog = function (editor) {
    let frame = getPreviewFrame(editor)
    const id = 'tinymce-editor-preview-dialog-wrapper'
    const closeEvent = `
      onclick="document.getElementById('${id}').remove()"
    `
    const dialog = document.createElement('div')
    dialog.id = id
    dialog.innerHTML = `
      <div class="tinymce-editor-preview-dialog-mask"></div>
      <div class="tinymce-editor-preview-dialog">
        <div class="tinymce-editor-preview-dialog-header">
            <div class="tinymce-editor-preview-dialog-header-title">預覽</div>
            <div class="tinymce-editor-preview-dialog-header-close" ${closeEvent} title="關閉">
              <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg">
                <path d="M17.953 7.453L13.422 12l4.531 4.547-1.406 1.406L12 13.422l-4.547 4.531-1.406-1.406L10.578 12
                 6.047 7.453l1.406-1.406L12 10.578l4.547-4.531z" fill-rule="evenodd"></path>
              </svg>
            </div>
        </div>
        <div class="tinymce-editor-preview-dialog-body">${frame}</div>
        <div class="tinymce-editor-preview-dialog-footer">
            <div class="tinymce-editor-preview-dialog-footer-btn" ${closeEvent}>關閉</div>
        </div>
      </div>
    `
    return dialog
  }

  const actionFunction = (editor, value) => {
    const dialog = getPreviewDialog(editor)
    if (document.getElementById(dialog.id)) {
      console.warn('當前頁面只能有一個彈窗')
    } else {
      document.body.appendChild(dialog)
    }
  }

  editor.addCommand('mcePreview', function () {
    actionFunction(editor)
  })

  editor.ui.registry.addButton('preview', {
    icon: 'preview',
    tooltip: 'Preview',
    onAction: function () {
      return editor.execCommand('mcePreview')
    }
  })
})

複製程式碼

這個外掛根據編輯器官方的預覽外掛改寫而成,定製化了一些樣式。如前所述,因為與官方外掛重名,所以官方的外掛檔案將不再會被載入。這個外掛比行高外掛稍微複雜了點,但是它沒有對內容進行改動。

音訊外掛

開發中

參考

  1. tinymce官方文件

相關文章