008前端阅读记录

tinymce本地化部署方案

乌漆嘛黑和 Ahri
第 008 期

前提说明

  • vue3
  • tinymce 7.2.1
  • tinymce-vue 6.0.1

准备工作

安装tinymce和tinymce-vue

npm install tinymce@7.2.1 tinymce-vue@6.0.1 --save

复制代码到public

为什么不直接引用node_modules的文件 Tiny does not recommend bundling tinymce and tinymce-vue with a module loader. Bundling these packages can be complex and error prone.

如果一定想用node_moudule 文件参考上面的链接说明即可。可以忽略复制文件这一步。

复制文件只保留min.js相关即可。可以根据自己的需求保留哪些文件即可。也可以存放在自己的cdn,使用远程链接。

public
tinymce
icons
default
icons.min.js
langs
zh_CN.js
models
dom
model.min.js
plugins
accordion
plugin.min.js
advlist
plugin.min.js
anchor
plugin.min.js
autolink
plugin.min.js
autoresize
plugin.min.js
autosave
plugin.min.js
charmap
plugin.min.js
code
plugin.min.js
codesample
plugin.min.js
directionality
plugin.min.js
emoticons
js
emojiimages.min.js
emojis.min.js
plugin.min.js
fullscreen
plugin.min.js
help
js
i18n
keynav
zh_CN.js
plugin.min.js
image
plugin.min.js
importcss
plugin.min.js
insertdatetime
plugin.min.js
link
plugin.min.js
lists
plugin.min.js
media
plugin.min.js
nonbreaking
plugin.min.js
pagebreak
plugin.min.js
preview
plugin.min.js
quickbars
plugin.min.js
save
plugin.min.js
searchreplace
plugin.min.js
table
plugin.min.js
visualblocks
plugin.min.js
visualchars
plugin.min.js
wordcount
plugin.min.js
skins
content
default
content.css
content.js
content.min.css
QZD
content.css
content.min.css
ui
QZD
fonts
tinymce-mobile.woff
content.css
content.inline.css
content.inline.min.css
content.min.css
content.mobile.css
content.mobile.min.css
skin.css
skin.min.css
skin.mobile.css
skin.mobile.min.css
themes
silver
theme.min.js
tinymce.min.js

新建组件

TinyMceEditor.vue

<script setup lang="ts">
import { ref, defineProps, computed } from 'vue'
import Editor from '@tinymce/tinymce-vue'
import type { RawEditorOptions } from 'tinymce'
import { getUploadParams, fileUpload } from '@/components/KzUpload/src/api'

defineOptions({
  name: 'BaseEditor'
})

interface ImageConfig {
  maxSize: number
  allowType: string[]
}

const props = defineProps<{
  /** 配置 */
  config: RawEditorOptions
  /** 值 */
  modelValue: string
  /** 图片限制 */
  imageConfig?: ImageConfig
}>()

const editorRef = ref()

// 图片上传自定义逻辑
/** 文档:https://www.tiny.cloud/docs/tinymce/latest/upload-images/ */
type UploadFn = RawEditorOptions['images_upload_handler']
const imageUploadHandler: UploadFn = async function (blobInfo, _) {
  return new Promise(async (resolve, reject) => {
    const { maxSize = 10 * 1024 * 1024, allowType = ['image/png', 'image/jpg', 'image/jpeg'] } =
      props.imageConfig || {}
    if (blobInfo.blob().size > maxSize) {
      const msg = `图片太大了. 最大支持 ${maxSize / 1024 / 1024}MB`
      console.error(msg, blobInfo.blob().size)
      reject({ message: msg, remove: true })
      return
    }
    if (!allowType.includes(blobInfo.blob().type)) {
      const msg = `图片只支持${allowType.map((item) => item.replace('image/', '').split('、'))}格式`
      reject({
        message: msg,
        remove: true
      })
      console.error(msg, blobInfo.blob().type)
      return
    }
    // 自己上传图片的逻辑开始
    let baseUrl = import.meta.env.VITE_BASE_URL
    const { data } = await getUploadParams({
      baseUrl,
      appCode: 'App',
      originalFilename: blobInfo.filename()
    })
    const res = await fileUpload(blobInfo.blob(), data, [], () => {})
    // 自己上传图片的逻辑结束
    resolve(res.data.downloadLocation)
    return res.data.downloadLocation
  })
}

// 双向绑定v-model
const modelContent = defineModel({ default: '' })

const defaultConfig = ref<RawEditorOptions>({
  // 如果不设置license_key为gpl,console会一直提示一个模式问题
  license_key: 'gpl',
  // 编辑器的高度
  height: 300,
  // 移除tinymce右上角升级提示
  promotion: false,
  skin: 'QZD',
  // 移除tinymce右下角品牌提示
  branding: false,
  // 设置语言包路径
  language_url: `/libs/tinymce/langs/zh_CN.js`,
  // 设置语言
  language: 'zh_CN',
  menubar: false,
  // 配置插件列表
  plugins:
    'preview importcss searchreplace autolink save directionality code visualblocks visualchars fullscreen image link table charmap pagebreak nonbreaking anchor insertdatetime advlist lists wordcount help charmap quickbars emoticons accordion',
  // 配置工具栏
  toolbar: [
    'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough | lineheight align numlist bullist | link image | table | forecolor backcolor removeformat | charmap emoticons | code fullscreen preview'
  ],
  setup: (editor) => {
    editor.on('PreInit', function () {
      editor.editorUpload.addFilter(function (image) {
        const maxSize = 10 * 1024
        const imageSize = image.width * image.height
        return imageSize < maxSize
      })
    })
  },
  // 内容默认样式
  content_style: '',
  // 配置图片上传功能
  images_upload_handler: imageUploadHandler
})

// 初始化配置
const initConfig = computed(() => {
  return {
    ...defaultConfig.value,
    ...props.config,
    setup: (editor: any) => {
      props.config?.setup?.(editor)
      defaultConfig.value?.setup?.(editor)
    }
  }
})

defineExpose({
  editorRef
})
</script>

<template>
  <Editor
    ref="editorRef"
    :init="initConfig"
    :tinymceScriptSrc="/libs/tinymce/tinymce.min.js"
    v-model="modelContent"
  />
</template>

<style>
.tox-tinymce {
  border: none !important;
}

.tox-shadowhost.tox-fullscreen,
.tox.tox-tinymce.tox-fullscreen {
  z-index: 2004 !important;
}
</style>

常见问题

如何做语言本地化?

上方的组件已经做了本地化操作。主要配置就是lang字段。可以去下载文档,language-packages, 如果需要自定义修改下载的文件即可。

如何添加自定义字体?

// TinyMceEditor.vue 文件中
const defaultConfig = {
  ...otherConfig,
    font_size_formats:
      '初号=56px 小初=48px 一号=34px 小一=32px 二号=29px 小二=24px 三号=21px 小三=20px 四号=18px 小四=16px 五号=14px 小五=12px 六号=10px',
    content_style: `@import url('your-cdn-domain/front-cdn/fonts/style/tinymce.css?v1');body { font-family:  SimHei; font-size: 14px; }`,
    font_family_formats:
      '黑体=SimHei;宋体=SimSun;微软雅黑=Microsoft YaHei,Helvetica,sans;Arial=Arial,Helvetica,sans-serif;Times New Roman=Times New Roman,Times;',
}
/* your-cdn-domain/front-cdn/fonts/style/tinymce.css?v1 */
@font-face {
    font-family: 'SimSun';
    src: url('../songti/Songti.ttc');
}

@font-face {
    font-family: 'Microsoft Yahei';
    src: url('../yahei/Microsoft-YaHei-Regular.ttc');
    font-weight: 400;
}

@font-face {
    font-family: 'Microsoft Yahei';
    src: url('../yahei/Microsoft-YaHei-Bold.ttc');
    font-weight: 700;
}

@font-face {
    font-family: 'SimHei';
    src: url('../heiti/STHeiti Light.ttc');
}
<!-- TinyMceEditor.vue 文件中 编辑器页面也要导入用于页面展示 -->
<style lang="scss">
@import url('your-cdn-domain/file-resource/dev/front-cdn/fonts/style/tinymce.css?v1');
</style>

如何添加自定义icon?

tinymce 的多套皮肤,是需要付费的,如果觉得不好看就自己改tinymce/icons/default/icons.min.js文件

如果只有个别自己新增的icon,可以自己使用api 添加,(只支持svg文件。)

editor.ui.registry.addIcon('triangleUp', '<svg height="24" width="24"><path d="M12 0 L24 24 L0 24 Z" /></svg>');

a 标签的href属性被编辑器自动修改了

具体原因尚未定位到,目前推测是link插件内部做了某些格式化导致的,具体表现为:

<a href="https://www.baidu.com?id=1&search=2&tag=3">https://www.baidu.com?id=1&search=2</a>

当以上内容通过insertContent方法插入到编辑器中时,再编辑器中回车操作时,当前内容会变成

<a href="https://www.baidu.com?id=1&search=2">https://www.baidu.com?id=1&search=2</a>

编辑器会自动将href更新为https://www.baidu.com?id=1&search=2, 也就是保持跟a标签内容一致,从而导致属性丢失。

最终解决方案是,将a标签的内容变成更href毫无关联的内容,就不会替换。

<a href="https://www.baidu.com?id=1&search=2&tag=3">百度搜索</a>

后续可以跟进此issue After insertContent 'a' tag, the href parameter is lost after pressing enter.