前言
本文是在Tinymce富文本编辑器添加自定义toolbar,二级菜单,自定义表单,签名的基础之上进行一些bug记录,功能添加,以及模版的应用和打印
项目描述
- 建立电子病历模版—录入(电子病历模版和电子病历打印模版)—查看电子病历和打印病历模版
- 建立电子病历----添加一个电子病历可以添加多个电子病历模版----并输入设置的值提交以key和value的组成的数组结构,提交,并在编辑时返显电子病历模版和数据----点击打印按钮,调出打印模版,赋值进行打印。
一、模版应用及打印
- 模版的应用
点击新增电子病历,选择用户,选择电子模版,展示。此时电子病历只填值,此处需要优化。
选择完顾客,顾客的信息需要返显在电子模版上,
-
解决的问题:
模版返显—拿到模版的key和value组成的数组—然后赋值—更新模版—在页面中展示
-
引出的问题:
1)输入后每次光标会到最左侧,输入出错。
原因封装的编辑器每次更新之后都会通过模版的key和value组成的数组,然后重新设置模版,所以光标返回最左侧
解决方法,定义变量只有一次拿到模版的key和value组成的数组,其他变更不触发emit返回新的数组,不重新给模版赋值
编辑组件的应用文章来源:https://www.toymoban.com/news/detail-846851.html<Editor ref="editorTinymce" height="900" v-model="templateData" // 模版的html :templatevalue="templatevalue" // 模版的key和value组成的数组 :isHide="true" // 是否展示菜单栏和toolbar栏 v-if="!!templateData" // 模版展示条件 :templateData="templateData" // 模版的html :getParams="getParams" // 是否需要获取key和value组成的数组 @templateparams="handleTemplateParams" // 模版返回的key和value组成的数组 />
- 模版的赋值
点击左侧已添加的病历,返显模版以及将模版之前输入的值进行返显
- 引出的问题:
1)切换模版,赋值有时不更新
赋值分2种,1.初始化赋值,2.变更赋值。
解决方法,第一次点击为初始化赋值,之后的点击为变更赋值,切换已添加的模版并没有销毁编辑器,所以是变更。文章来源地址https://www.toymoban.com/news/detail-846851.html
- 模版的打印
点击编辑页打印—调出打印模版—将录入模版的值赋值给打印模版
首先:输入模版和打印模版对应的值需要设置对应的key,如果不一样会赋值失败,此处属于初化始化赋值。
点击列表操作列的打印—弹出所有关联的模版—选中模版,给打印模版赋值,进行打印预览 - 模版的销毁
关闭弹窗之后,需要销毁模版,否者再次打开时之前打开的模版还在。
封装组件代码
<template>
<div :class="prefixCls" :style="{ width: containerWidth }">
<ImgUpload
:fullscreen="fullscreen"
@uploading="handleImageUploading"
@done="handleDone"
v-if="showImageUpload && !props.isHide"
v-show="editorRef"
:disabled="disabled"
:uploadParams="props.uploadParams"
/>
<textarea :id="tinymceId" ref="elRef" :style="{ visibility: 'hidden' }" v-if="!initOptions.inline"></textarea>
<slot v-else></slot>
<signModal @register="signatureModal" @success="handleSignature" @exportSign="getSign" />
<recordModal @register="recorderModal" @success="handleGetText" />
</div>
</template>
<script lang="ts">
import sign from '/@/assets/svg/sign.svg';
import recordSvg from '/@/assets/svg/record.svg';
import type { Editor, RawEditorSettings, BodyComponentSpec } from 'tinymce';
import { useMessage } from '/@/hooks/web/useMessage';
import tinymce from 'tinymce/tinymce';
import 'tinymce/themes/silver';
import 'tinymce/icons/default/icons';
import 'tinymce/plugins/advlist';
import 'tinymce/plugins/anchor';
import 'tinymce/plugins/autolink';
import 'tinymce/plugins/autosave';
import 'tinymce/plugins/code';
import 'tinymce/plugins/codesample';
import 'tinymce/plugins/directionality';
import 'tinymce/plugins/fullscreen';
import 'tinymce/plugins/hr';
import 'tinymce/plugins/insertdatetime';
import 'tinymce/plugins/link';
import 'tinymce/plugins/lists';
import 'tinymce/plugins/media';
import 'tinymce/plugins/nonbreaking';
import 'tinymce/plugins/noneditable';
import 'tinymce/plugins/pagebreak';
import 'tinymce/plugins/paste';
import 'tinymce/plugins/preview';
import 'tinymce/plugins/print';
import 'tinymce/plugins/save';
import 'tinymce/plugins/searchreplace';
import 'tinymce/plugins/spellchecker';
import 'tinymce/plugins/tabfocus';
import 'tinymce/plugins/table';
import 'tinymce/plugins/template';
import 'tinymce/plugins/textpattern';
import 'tinymce/plugins/visualblocks';
import 'tinymce/plugins/visualchars';
import 'tinymce/plugins/wordcount';
// import '/@/components/MedicalTinymce/plugins/control/index.js';
import { defineComponent, computed, nextTick, ref, unref, watch, onDeactivated, onBeforeUnmount, toRaw } from 'vue';
import ImgUpload from './ImgUpload.vue';
import { toolbar, plugins } from './tinymce';
import { buildShortUUID } from '/@/utils/uuid';
import { bindHandlers } from './helper';
import { useModal } from '/@/components/Modal';
import { ActionEnum, VALIDATE_API } from '/@/enums/commonEnum';
import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated';
import { useDesign } from '/@/hooks/web/useDesign';
import { isNumber } from '/@/utils/is';
import { useLocale } from '/@/locales/useLocale';
import { useAppStore } from '/@/store/modules/app';
import { asyncFindDefUrlById, asyncFindUrlById } from '/@/api/lamp/file/upload';
import signModal from '/@/components/Signature/components/signModal/index.vue';
import recordModal from '/@/components/CustomRecorder/index.vue';
const tinymceProps = {
options: {
type: Object as PropType<Partial<RawEditorSettings>>,
default: () => ({}),
},
value: {
type: String,
},
toolbar: {
type: Array as PropType<string[]>,
default: toolbar,
},
plugins: {
type: Array as PropType<string[]>,
default: plugins,
},
modelValue: {
type: String,
},
height: {
type: [Number, String] as PropType<string | number>,
required: false,
default: 400,
},
width: {
type: [Number, String] as PropType<string | number>,
required: false,
default: 'auto',
},
showImageUpload: {
type: Boolean,
default: true,
},
isDef: {
type: Boolean,
default: false,
},
uploadParams: {
type: Object as PropType<any>,
default: {},
},
isHide: {
type: Boolean,
default: false,
},
isPrint: {
type: Boolean,
default: false,
},
templatevalue: {
type: Array,
},
getParams: {
type: Boolean,
default: false,
},
};
export default defineComponent({
name: 'Tinymce',
components: { ImgUpload, signModal, recordModal },
inheritAttrs: false,
props: tinymceProps,
emits: ['change', 'update:modelValue', 'inited', 'init-error', 'templateparams'],
setup(props, { emit, attrs }) {
const { createMessage } = useMessage();
const editorRef = ref<Nullable<Editor>>(null);
const fullscreen = ref(false);
const tinymceId = ref<string>(buildShortUUID('tiny-vue'));
const elRef = ref<Nullable<HTMLElement>>(null);
let dialogConfig = ref(null);
const { prefixCls } = useDesign('tinymce-container');
const [signatureModal, { openModal: openSignModal }] = useModal();
const [recorderModal, { openModal: openRecord }] = useModal();
const appStore = useAppStore();
const appEnv = import.meta.env.MODE;
let currentBookMark = ref<any>('');
const tinymceContent = computed(() => props.modelValue);
const childBtn = {
type: 'grid', // component type
columns: 1, // number of columns
items: [
{
type: 'button',
name: 'add',
text: '添加子项',
},
{
type: 'button',
name: 'del',
text: '删除子项',
},
{
type: 'collection', // component type
name: 'collection', // identifier
label: '',
},
{
type: 'collection', // component type
name: 'collection1', // identifier
label: '',
},
], // array of panel components
};
let childItem = {
type: 'grid', // component type
columns: 1, // number of columns
items: [
{
type: 'grid',
columns: 2,
items: [
{
type: 'input',
name: 'label1',
label: '标签1',
},
{
type: 'input',
name: 'value1',
label: '值1',
},
],
},
], // array of panel components
};
const containerWidth = computed(() => {
const width = props.width;
if (isNumber(width)) {
return `${width}px`;
}
return width;
});
const skinName = computed(() => {
return appStore.getDarkMode === 'light' ? 'oxide' : 'oxide-dark';
});
const langName = computed(() => {
const lang = useLocale().getLocale.value;
return ['zh_CN', 'en'].includes(lang) ? lang : 'zh_CN';
});
const initOptions = computed((): RawEditorSettings => {
const { height, options, toolbar, plugins } = props;
const publicPath = import.meta.env.VITE_PUBLIC_PATH || '/';
return {
selector: `#${unref(tinymceId)}`,
height,
// toolbar: appEnv === 'development' ? [...toolbar, 'HtmlBtn'] : toolbar,
toolbar: !!props.isHide ? false : !!props.isPrint ? false : toolbar,
menubar: !!props.isHide ? false : !!props.isPrint ? 'print' : 'file edit insert view format table',
menu: {
print: {
title: '打印',
items: 'print',
},
},
plugins,
fontsize_formats: '8pt 10pt 12pt 14pt 16pt 18pt 20pt 22pt 24pt 36pt',
font_formats: `微软雅黑='微软雅黑';宋体='宋体';黑体='黑体';仿宋='仿宋';楷体='楷体';隶书='隶书';幼圆='幼圆';
Andale Mono=andale mono,times;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;
Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,
courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,
arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Trebuchet MS=trebuchet
ms,geneva;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings`,
language_url: publicPath + 'resource/tinymce/langs/' + langName.value + '.js',
language: langName.value,
branding: false,
default_link_target: '_blank',
link_title: false,
object_resizing: false,
auto_focus: true,
skin: skinName.value,
skin_url: publicPath + 'resource/tinymce/skins/ui/' + skinName.value,
content_css: publicPath + 'resource/tinymce/skins/ui/' + skinName.value + '/content.min.css',
...options,
extended_valid_elements: 'a[class|target|href|onclick],div[class|onclick|id|style],link[rel|href]',
setup: (editor: Editor) => {
console.log(editor, 'editoreditoreditoreditoreditor');
editorRef.value = editor;
editor.on('init', (e) => initSetup(e));
// 注册一个icon
editor.ui.registry.addIcon(
'shopping-cart',
`<svg viewBox="0 0 1024 1024" data-icon="shopping-cart" width="1.5em" height="1.5em" fill="currentColor" aria-hidden="true" focusable="false" class=""><path d="M922.9 701.9H327.4l29.9-60.9 496.8-.9c16.8 0 31.2-12 34.2-28.6l68.8-385.1c1.8-10.1-.9-20.5-7.5-28.4a34.99 34.99 0 0 0-26.6-12.5l-632-2.1-5.4-25.4c-3.4-16.2-18-28-34.6-28H96.5a35.3 35.3 0 1 0 0 70.6h125.9L246 312.8l58.1 281.3-74.8 122.1a34.96 34.96 0 0 0-3 36.8c6 11.9 18.1 19.4 31.5 19.4h62.8a102.43 102.43 0 0 0-20.6 61.7c0 56.6 46 102.6 102.6 102.6s102.6-46 102.6-102.6c0-22.3-7.4-44-20.6-61.7h161.1a102.43 102.43 0 0 0-20.6 61.7c0 56.6 46 102.6 102.6 102.6s102.6-46 102.6-102.6c0-22.3-7.4-44-20.6-61.7H923c19.4 0 35.3-15.8 35.3-35.3a35.42 35.42 0 0 0-35.4-35.2zM305.7 253l575.8 1.9-56.4 315.8-452.3.8L305.7 253zm96.9 612.7c-17.4 0-31.6-14.2-31.6-31.6 0-17.4 14.2-31.6 31.6-31.6s31.6 14.2 31.6 31.6a31.6 31.6 0 0 1-31.6 31.6zm325.1 0c-17.4 0-31.6-14.2-31.6-31.6 0-17.4 14.2-31.6 31.6-31.6s31.6 14.2 31.6 31.6a31.6 31.6 0 0 1-31.6 31.6z"></path></svg>`,
);
// 注册获取html以及数据的按钮
registerSignBtn(editor);
},
// 生命周期:挂载后回调
init_instance_callback: (editor: Editor) => {
// 修改编辑器默认字体和字号
editor.getBody().style.fontSize = '16pt';
editor.getBody().style.fontFamily = '宋体';
},
};
});
// 注册获取html以及数据的按钮
function registerSignBtn(editor: Editor) {
editor.ui.registry.addButton('CardBtn', {
type: 'button',
// icon: `shopping-cart`,
text: '获取并保存html',
onAction: function (_) {
//按钮事件:组装 html + data数据
getControlValue();
saveTemplate(editor.getContent(), getControlValue());
},
});
}
// 获取控件数据值
function getControlValue() {
let dom = tinymce.activeEditor.dom;
let controls = dom.select('.control');
let data = controls.map((item) => {
// console.log('item', item);
let dataControl = JSON.parse(item.getAttribute('data-control'));
let controlValue = item.getAttribute('data-value');
//文本框 没有data-value
console.log(controlValue, item.firstElementChild.innerHTML, 'item.firstElementChild.innerHTML');
if (!controlValue) {
if (dataControl.initialData.select == 'input') {
if (!!item.firstElementChild.innerHTML) {
controlValue = item.firstElementChild.innerHTML;
} else {
controlValue = '';
}
}
// controlValue = item.firstElementChild.innerHTML;
}
return {
controlType: dataControl.initialData.select,
fieldName: dataControl.initialData.name,
fieldKey: dataControl.initialData.fieldKey,
controlValue,
};
});
console.log(data);
return data;
}
// 保存模板
async function saveTemplate(doc: string, data: any) {
try {
const params: any = { doc, data };
emit('templateparams', params);
} finally {
}
}
const disabled = computed(() => {
const { options } = props;
const getdDisabled = options && Reflect.get(options, 'readonly');
const editor = unref(editorRef);
if (editor) {
editor.setMode(getdDisabled ? 'readonly' : 'design');
}
return getdDisabled ?? false;
});
watch(
() => attrs.disabled,
() => {
const editor = unref(editorRef);
if (!editor) {
return;
}
editor.setMode(attrs.disabled ? 'readonly' : 'design');
},
);
onMountedOrActivated(() => {
if (!initOptions.value.inline) {
tinymceId.value = buildShortUUID('tiny-vue');
}
nextTick(() => {
setTimeout(() => {
initEditor();
}, 30);
});
});
onBeforeUnmount(() => {
destory();
});
onDeactivated(() => {
destory();
});
function destory() {
if (tinymce !== null) {
tinymce?.remove?.(unref(initOptions).selector!);
}
}
// 弹框配置
let Dialog = (editor) => {
return {
title: '添加控件', // The dialog's title - displayed in the dialog header
body: {
type: 'panel', // The root body type - a Panel or TabPanel
items: [
// A list of panel components
{
type: 'selectbox',
name: 'select',
label: '控件',
items: [
{ value: 'input', text: '输入框' },
{ value: 'date', text: '日期' },
{ value: 'radio', text: '单选框' },
{ value: 'checkbox', text: '多选框' },
{ value: 'select', text: '下拉' },
{ value: 'sign', text: '签名' },
{ value: 'record', text: '语音转换' },
// { value: 'textarea', text: 'textarea' },
],
},
{
type: 'input', //类型可以是 checkbox, input, selectbox, textarea and urlinput
name: 'name',
label: '字段名称',
},
{
type: 'input', //类型可以是 checkbox, input, selectbox, textarea and urlinput
name: 'fieldKey',
label: '字段key值',
},
{
type: 'selectbox', //类型可以是 checkbox, input, selectbox, textarea and urlinput
name: 'isShow',
label: '字段名称是否展示',
items: [
{ value: '1', text: '展示' },
{ value: '2', text: '不展示' },
],
},
// {
// type: 'checkbox',
// name:'checkbox',
// label:'checkbox'
// },
// {
// type: 'button',
// name:'tianjia',
// text:'添加子项',
// disabled:false
// },
// {
// type: 'htmlpanel', // A HTML panel component
// html: '11'
// },
],
},
//初始值
// initialData: {
// name: '2'
// },
buttons: [
// A list of footer buttons
{
type: 'cancel',
name: 'closeButton',
text: '取消',
},
{
type: 'submit',
primary: true,
text: '确认',
},
],
// radio select checkbox 添加子项
onAction: dialogFn.onAction(editor),
// 切换表单控件
onChange: dialogFn.onChange(editor),
onSubmit: dialogFn.onSubmit(editor),
};
};
function initEditor() {
const el = unref(elRef);
if (el) {
el.style.visibility = '';
}
tinymce
.init(unref(initOptions))
.then((editor) => {
emit('inited', editor);
})
.catch((err) => {
emit('init-error', err);
});
tinymce.PluginManager.add('control', function (editor) {
dialogConfig.value = Dialog(editor);
const dialogOpener = () => {
return editor.windowManager.open(dialogConfig.value);
};
editor.ui.registry.addButton('control', {
icon: 'non-breaking', //图标
tooltip: '插入控件', //提示
text: '控件库',
onAction: function () {
dialogOpener();
},
});
setupButtons(editor);
if (!props.isHide) {
addToEditor(editor);
}
initBindEvent(editor);
});
}
// 为初始化内容中控件绑定事件 和 控件值回填
let initBindEvent = (editor) => {
console.log(props.templatevalue, 'propspropspropsprops');
let templateValue = toRaw(props.templatevalue);
editor.on('init', () => {
//绑定事件
bindEvent(editor);
//控件值回填
let dom = tinymce.activeEditor.dom;
let controls = dom.select('.control');
controls.forEach((item) => {
let dataControl = JSON.parse(item.getAttribute('data-control'));
let controlValue = item.getAttribute('data-value');
if (!!templateValue && templateValue.length > 0) {
templateValue.forEach((i) => {
if (i.fieldKey == dataControl.initialData.fieldKey) {
controlValue = i.controlValue;
switch (dataControl.initialData.select) {
case 'input':
if (!!controlValue) {
console.log(item, item.firstElementChild, 'itemitemitemitemitem');
if (!!item.firstElementChild) {
item.firstElementChild.innerText = controlValue;
}
// item.setAttribute('data-value', controlValue);
}
break;
case 'radio':
if (!!controlValue) {
item.querySelector('[value="' + controlValue + '"]').checked = true;
}
break;
case 'checkbox':
let checkboxs = item.querySelectorAll('input');
checkboxs.forEach((it) => {
if (controlValue.split(',').includes(it.value)) {
it.checked = true;
}
});
break;
case 'select':
item.querySelector('[value=' + controlValue + ']').selected = true;
break;
}
}
});
} else {
switch (dataControl.initialData.select) {
case 'radio':
if (!!controlValue) {
item.querySelector('[value="' + controlValue + '"]').checked = true;
}
break;
case 'checkbox':
let checkboxs = item.querySelectorAll('input');
checkboxs.forEach((it) => {
if (controlValue.split(',').includes(it.value)) {
it.checked = true;
}
});
break;
case 'select':
item.querySelector('[value=' + controlValue + ']').selected = true;
break;
}
}
console.log(controlValue);
});
});
};
let addToEditor = (editor) => {
// 添加悬浮 上下文工具栏
editor.ui.registry.addContextToolbar('editcontrol', {
//触发条件
predicate: function (node) {
console.log(node, props.isHide, 'isHideisHideisHide');
// alert(node);
// if (node.className == 'c-sign') {
// openSignModal(true, {
// type: ActionEnum.ADD,
// });
// }
return !props.isHide && node.className === 'control';
// return !props.isHide && node.className === 'control' && node.nodeName.toLowerCase() === 'span';
},
items: 'changecontrol removecontrol', //显示的工具列表
position: 'selection', //工具栏放置位置 selection node line
// scope: 'node',
});
};
// 弹框中的方法
let dialogFn = {
// radio select checkbox 添加子项编辑控件
onAction: (edntor) => (dialogApi, details) => {
let data = dialogApi.getData();
if (details.name == 'add') {
addChildItem(dialogApi);
}
if (details.name == 'del') {
let items = dialogConfig.value.body.items;
items[items.length - 1].items.pop();
}
dialogConfig.value.initialData = data;
dialogApi.redial(dialogConfig.value);
},
// 控件弹窗选择控件Change事件
onChange: (editor) => (dialogApi, details) => {
console.log(dialogConfig.value.body, details, dialogApi);
if (dialogConfig.value.title == '编辑控件') return;
let data = dialogApi.getData();
// dialogConfig.body.items[4].html = formControl[data.select]
if (data.select == 'input' || data.select == 'date' || data.select == 'textarea') {
dialogConfig.value.body.items.splice(4);
// dialogApi.redial(dialogConfig.value);
// dialogApi.setData(data);
}
if (
data.select != 'input' &&
data.select != 'date' &&
data.select != 'textarea' &&
data.select != 'sign' &&
data.select != 'record' &&
!dialogConfig.value.body.items[4]
) {
let btns = JSON.parse(JSON.stringify(childBtn));
let items = JSON.parse(JSON.stringify(childItem));
dialogConfig.value.body.items.splice(4, 0, btns, items);
dialogApi.redial(dialogConfig.value);
dialogApi.setData(data);
}
// dialogApi.redial(dialogConfig.value);
// if (details.name == 'select') dialogApi.redial(dialogConfig.value); //重新渲染dialog
// dialogApi.setData(data);
// dialogApi.focus(); // 聚焦
console.log('dataTdataTdataT', data);
},
// 控件弹窗确认回调事件
onSubmit: (editor) => (api) => {
let control = '';
let data = api.getData();
let controlName = data.select + getId();
// 输入框
if (data.select == 'input') {
control = `<span contenteditable="true" style="display:inline-block;
min-width:100px;border-bottom:1px solid black;outline: none;padding: 0" name=${controlName} type="${data.select}"> </span>`;
} else if (data.select == 'date') {
// 日期
control = `<input class="c-form" contenteditable="true" style='border: none;border-bottom: 1px solid' name=${controlName} type="datetime-local" value="${data.name}" />`;
} else if (data.select == 'textarea') {
//文本御
control = `<textarea class="c-form" contenteditable="true" rows="2" cols="30" style="display:inline-block;
min-width:100px;border-bottom:1px solid;" name=${controlName}></textarea>`;
}
// if(data.select == 'input' || data.select == 'date'){
// control = `<input contenteditable="true" name=${controlName} type="${data.select}" value="${data.name}" />`
// }
else if (data.select == 'radio' || data.select == 'checkbox') {
let l = dialogConfig.value.body.items[dialogConfig.value.body.items.length - 1].items.length;
for (let i = 1; i <= l; i++) {
control =
control +
`<label contenteditable="true"><input class="c-form" name=${controlName} type=${data.select} value=${data['value' + i]} /> ${
data['label' + i]
}</label>`;
}
} else if (data.select == 'select') {
// 下拉
let l = dialogConfig.value.body.items[dialogConfig.value.body.items.length - 1].items.length;
for (let i = 1; i <= l; i++) {
control = control + `<option value=${data['value' + i]} label=${data['label' + i]}></option>`;
}
control = `<select class="c-form" contenteditable="true" style='border: none;border-bottom: 1px solid;padding: 5px 0px 5px 5px' name=${controlName}>${control}</select>`;
} else if (data.select === 'sign') {
control = `<img style="width: 32px;height: 32px;" class='c-sign' src='${sign}'></img>`;
} else if (data.select === 'record') {
control = `<img style="width: 32px;height: 32px;" class='c-record' src='${recordSvg}'></img>`;
}
// 通用 dom 结构和样式
control = `${
!!data.name && data.isShow == '1' && dialogConfig.value.title != '编辑控件' ? `<span>${data.name}</span>:` : ``
}<span class="control" id="span1"
style="display:inline-block;margin:0 5px;background-color: #f1f1f1;"
contenteditable="false" data-control=${JSON.stringify({ body: dialogConfig.value.body, initialData: data })} data-value="">
${control}
<span class="c-menu" style="padding:0 5px;"></span>
</span></span>`;
// console.log(editor.selection.getNode())
if (editor.selection.getContent()) {
//编辑控件
// editor.selection.getNode().parentNode.removeChild(editor.selection.getNode());
editor.selection.setContent(control);
// editor.insertContent(control)
} else {
//添加控件
editor.insertContent(control);
}
bindEvent(editor);
api.close();
},
};
// 为控件绑定事件
let bindEvent = (editor) => {
console.log(navigator.userAgent, 'editoreditoreditor');
let dom = tinymce.activeEditor.dom;
// console.log(dom.select('#span1'))
setTimeout(() => {
// dom.bind(dom.select('.c-menu'), 'click', (e) => {
// // 显示指定的上下文菜单
// editor.dispatch('contexttoolbar-show', { toolbarKey: 'editcontrol' });
// // 隐藏指定的上下文菜单
// editor.dispatch('contexttoolbar-hide', { toolbarKey: 'editcontrol' });
// e.stopPropagation();
// });
dom.bind(dom.select('.c-form'), 'change', (e) => {
if (e.target.type == 'date') {
e.target.parentNode.setAttribute('data-value', e.target.value);
e.target.setAttribute('value', e.target.value);
}
if (e.target.type == 'radio') {
e.target.parentNode.parentNode.querySelectorAll('label').forEach((item) => {
item.querySelector('input').removeAttribute('checked');
});
e.target.parentNode.parentNode.setAttribute('data-value', e.target.value);
e.target.setAttribute('checked', 'checked');
}
if (e.target.type == 'checkbox') {
let checkedArr = [];
let parentSpan = e.target.parentNode.parentNode;
// ---------------------- 值响应有问题
parentSpan.querySelectorAll('label').forEach((item) => {
let checkbox = item.querySelector('input');
// checkbox.removeAttribute('checked')
if (checkbox.checked) {
checkedArr.push(checkbox);
// checkbox.setAttribute('checked','checked')
}
});
parentSpan.setAttribute('data-value', checkedArr.toString());
}
if (e.target.localName == 'select') {
e.target.querySelectorAll('option').forEach((item) => {
item.removeAttribute('selected');
if (item.selected) {
item.setAttribute('selected', 'selected');
}
});
e.target.parentNode.setAttribute('data-value', e.target.value);
}
});
// 单独为 日期 控件绑定失焦事件,change不好使
dom.bind(dom.select('.c-form'), 'blur', (e) => {
if (e.target.type == 'datetime-local') {
const time = new Date(e.target.value).toLocaleString();
e.target.parentNode.setAttribute('data-value', time);
e.target.setAttribute('value', e.target.value);
}
});
//签名点击事件
dom.bind(dom.select('.c-sign'), `click`, () => {
currentBookMark.value = editor.selection.getBookmark(); //记录点击的位置,弹框结束后插入签名
openSignModal(true, {
type: ActionEnum.ADD,
});
// editor.selection.setContent(sign); //把签名插入编辑器
});
// 签名ipad touch事件
dom.bind(dom.select('.c-sign'), `touchstart`, (e) => {
console.log(e);
currentBookMark.value = editor.selection.getBookmark(); //记录点击的位置,弹框结束后插入签名
openSignModal(true, {
type: ActionEnum.ADD,
});
// editor.selection.setContent(sign); //把签名插入编辑器
});
// 语音转换事件
dom.bind(dom.select('.c-record'), `click`, () => {
currentBookMark.value = editor.selection.getBookmark(); //记录点击的位置,弹框结束后插入文字
openRecord(true, {
type: ActionEnum.ADD,
});
// editor.selection.setContent(sign); //把签名插入编辑器
});
// ipad touch事件
dom.bind(dom.select('.c-record'), `touchstart`, (e) => {
console.log(e);
currentBookMark.value = editor.selection.getBookmark();
openRecord(true, {
type: ActionEnum.ADD,
});
// editor.selection.setContent(sign); //把签名插入编辑器
});
}, 100);
};
// 添加子项 key value 方法
function addChildItem(dialogApi) {
console.log(dialogApi, 'dialogApidialogApidialogApidialogApi');
let childItems = dialogConfig.value.body.items[dialogConfig.value.body.items.length - 1].items;
childItems.push({
type: 'grid', // component type
columns: 2, // number of columns
items: [
{
type: 'input',
name: 'label' + (childItems.length + 1),
label: '标签' + (childItems.length + 1),
},
{
type: 'input',
name: 'value' + (childItems.length + 1),
label: '值' + (childItems.length + 1),
},
], // array of panel components
});
}
// 添加上下文工具栏
let setupButtons = (editor) => {
editor.ui.registry.addButton('changecontrol', {
icon: 'edit-block',
tooltip: '编辑控件',
onAction: () => {
console.log(editor);
// editor.windowManager.open(dialogConfig)
// editor.setContent('1213')
// editor.selection.select(123)
// editor.selection.setContent('<span>123</span>')
// editor.selection.getNode()
console.log(editor.selection.getNode());
let data = editor.selection.getNode().getAttribute('data-control');
// let name = editor.selection.getNode().querySelector('input').getAttribute('name')
// let val = editor.selection.getNode().querySelectorAll('[name='+name+']').value
let dataJson = JSON.parse(data);
dialogConfig.value = Dialog(editor);
// dialogConfig.value.body = data.body;
dialogConfig.value.title = '编辑控件';
dialogConfig.value.initialData = dataJson.initialData;
if (dataJson.initialData.select == 'radio' || dataJson.initialData.select == 'checkbox') {
let btns = JSON.parse(JSON.stringify(childBtn));
var keyArr: string[] = [];
for (var key in dataJson.initialData) {
if (key.indexOf('label') > -1) {
keyArr.push(key);
}
}
console.log(keyArr);
let obj = {
type: 'grid', // component type
columns: 1, // number of columns
items: [],
};
keyArr.forEach((i) => {
let index = i.indexOf('label');
let strI = i.substring(5);
console.log(i, index, strI, 'strIstrI');
let itemObj = {
type: 'grid',
columns: 2,
items: [
{
type: 'input',
name: i,
label: '标签' + strI,
},
{
type: 'input',
name: 'value' + strI,
label: '值' + strI,
},
],
};
obj.items.push(itemObj);
});
let items = JSON.parse(JSON.stringify(obj));
dialogConfig.value.body.items.splice(4, 0, btns, items);
}
console.log(dialogConfig.value, 'dialogConfig.valuedialogConfig.valuedialogConfig.value');
editor.windowManager.open(dialogConfig.value);
},
});
editor.ui.registry.addButton('removecontrol', {
icon: 'remove',
tooltip: '删除控件',
onAction: (editor) => {
editor.selection.setContent('');
editor.dispatch('contexttoolbar-hide', { toolbarKey: 'editcontrol' });
console.log(arguments);
},
});
};
// 获取id name
function getId() {
return '_' + new Date().getTime();
}
function initSetup(e) {
const editor = unref(editorRef);
console.log(editor, 'editoreditoreditoreditor');
if (!editor) {
return;
}
const value = props.modelValue || '';
editor.setContent(value);
bindModelHandlers(editor);
bindHandlers(e, attrs, unref(editorRef));
}
function setValue(editor: Recordable, val: string, prevVal?: string) {
// console.log(editor, val, props.templatevalue, 'valvalvalval1111111111111111111111111');
// console.log(controls, 'controlscontrolscontrols');
if (editor && typeof val === 'string' && val !== prevVal && val !== editor.getContent({ format: attrs.outputFormat })) {
editor.setContent(val);
}
if (!!props.getParams) {
saveTemplate(editor.getContent(), getControlValue());
}
}
function bindModelHandlers(editor: any) {
const modelEvents = attrs.modelEvents ? attrs.modelEvents : null;
const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents;
watch(
() => props.modelValue,
(val: string, prevVal: string) => {
console.log(val, '00000000000000000000');
setValue(editor, val, prevVal);
},
);
watch(
() => props.value,
(val, prevVal) => {
console.log(val, prevVal, 'vvvvvvvvvvvvvvvvvvvv');
setValue(editor, val, prevVal);
},
{
immediate: true,
},
);
watch(
() => props.templatevalue,
(val, prevVal) => {
console.log(val, prevVal, '1111111111111111');
// setTemplateData(editor, val);
},
{
immediate: true,
},
);
editor.on(normalizedEvents ? normalizedEvents : 'change keyup undo redo', () => {
const content = editor.getContent({ format: attrs.outputFormat });
emit('update:modelValue', content);
emit('change', content);
});
editor.on('FullscreenStateChanged', (e) => {
fullscreen.value = e.state;
});
}
function setTemplateData(arr) {
let templateValue = toRaw(arr);
let dom = tinymce.activeEditor.dom;
let controls = dom.select('.control');
controls.forEach((item) => {
let dataControl = JSON.parse(item.getAttribute('data-control'));
let controlValue = item.getAttribute('data-value');
if (!!templateValue && templateValue.length > 0) {
templateValue.forEach((i) => {
if (i.fieldKey == dataControl.initialData.fieldKey) {
controlValue = i.controlValue;
switch (dataControl.initialData.select) {
case 'input':
if (!!controlValue) {
console.log(item, item.firstElementChild, 'itemitemitemitemitem');
if (!!item.firstElementChild) {
item.firstElementChild.innerText = controlValue;
}
// item.setAttribute('data-value', controlValue);
}
break;
case 'radio':
if (!!controlValue) {
item.querySelector('[value="' + controlValue + '"]').checked = true;
}
break;
case 'checkbox':
let checkboxs = item.querySelectorAll('input');
checkboxs.forEach((it) => {
if (controlValue.split(',').includes(it.value)) {
it.checked = true;
}
});
break;
case 'select':
item.querySelector('[value=' + controlValue + ']').selected = true;
break;
}
}
});
} else {
switch (dataControl.initialData.select) {
case 'radio':
if (!!controlValue) {
item.querySelector('[value="' + controlValue + '"]').checked = true;
}
break;
case 'checkbox':
let checkboxs = item.querySelectorAll('input');
checkboxs.forEach((it) => {
if (controlValue.split(',').includes(it.value)) {
it.checked = true;
}
});
break;
case 'select':
item.querySelector('[value=' + controlValue + ']').selected = true;
break;
}
}
console.log(controlValue);
});
// console.log(data, '000000000000001111111111111111111');
// editor.setContent(content);
// console.log(dom, '99999999999999999');
console.log(controls[0], '666666666666666666666666');
}
function handleImageUploading(name: string) {
const editor = unref(editorRef);
if (!editor) {
return;
}
editor.execCommand('mceInsertContent', false, getUploadingImgName(name));
const content = editor?.getContent() ?? '';
setValue(editor, content);
}
function handleDone(name: string, fileId: string) {
const editor = unref(editorRef);
if (!editor) {
return;
}
const content = editor?.getContent() ?? '';
if (fileId) {
const api = props.isDef ? asyncFindDefUrlById : asyncFindUrlById;
api(fileId).then((res) => {
// bug: 这里返回的图片链接必须是永久有效的,否则会出现图片过期无法访问的情况。 暂时没好的解决方案
if (res.code === 0) {
const val = content?.replace(getUploadingImgName(name), `<img data-id="${fileId}" src="${res?.data}"/>`) ?? '';
setValue(editor, val);
}
});
} else {
const val = content?.replace(getUploadingImgName(name), `<img data-path="${fileId}" src="${fileId}" alt="上传失败"/>`) ?? '';
setValue(editor, val);
}
}
function getUploadingImgName(name: string) {
return `[uploading:${name}]`;
}
function clickSetValue(val) {
const editor = unref(editorRef);
setValue(editor, val);
}
function handleSignature(data, base64Data) {
editorRef.value?.selection.moveToBookmark(currentBookMark.value); // 把光标位置摆正
editorRef.value?.focus(); // 聚焦
editorRef.value.execCommand(
// 填充!搞定!
'mceInsertContent',
false,
`<img style='height: 80px;width: 250px;' data-id="${data.id}" src="${data}" alt=""/>`,
);
// editorRef.value?.selection.setContent(`<img style='height: 80px;width: 250px' data-id="${data.id}" src="${data}" alt=""/>`);
}
// 获取语音转换后的文字
function handleGetText(data?) {
console.log(data);
}
function getSign(data: any) {
console.log(data);
}
function getSubmitData() {
const editor = unref(editorRef);
let controlValue = getControlValue();
let content = editor?.getContent() ?? '';
let data = {
doc: content,
data: controlValue,
};
return data;
}
function destroyTiny() {
tinymce.editors[unref(tinymceId)].destroy();
}
return {
prefixCls,
containerWidth,
initOptions,
signatureModal,
tinymceContent,
elRef,
tinymceId,
handleImageUploading,
handleDone,
editorRef,
fullscreen,
disabled,
props,
setValue,
clickSetValue,
handleSignature,
getSign,
recorderModal,
handleGetText,
getSubmitData,
setTemplateData,
destroyTiny,
};
},
});
</script>
<style lang="less" scoped></style>
<style lang="less">
@prefix-cls: ~'@{namespace}-tinymce-container';
.@{prefix-cls} {
position: relative;
line-height: normal;
textarea {
z-index: -1;
visibility: hidden;
}
}
</style>
总结
- 需要优化模版应用的时候,达到的效果,只输入,不能对其他的模版设置进行修改,目前可以对模版进行增删改。
- 模版打印的存在同1的问题
- tinymce的中文文档
- tinymce的英文Api
到了这里,关于Tinymce富文本编辑器二次开发电子病历时解决的bug的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!