253 lines
7.8 KiB
Vue
253 lines
7.8 KiB
Vue
<script>
|
|
// 轻量富文本输入:用浏览器编辑命令维护选区,并在模型输入/输出边界清洗 HTML。
|
|
import "~/assets/styles/RichTextEditor.NkmgPT5m.css";
|
|
import { renderList } from "vue";
|
|
import { onMounted } from "vue";
|
|
import { normalizeClass } from "vue";
|
|
import { mergeModels } from "vue";
|
|
import { useModel } from "vue";
|
|
import { watch } from "vue";
|
|
import { ref } from "vue";
|
|
import { createElementBlock } from "vue";
|
|
import { Fragment } from "vue";
|
|
import { createElementVNode } from "vue";
|
|
import { defineComponent } from "vue";
|
|
import { openBlock } from "vue";
|
|
import { normalizeStyle } from "vue";
|
|
import { withModifiers } from "vue";
|
|
import { t as _plugin_vue_export_helper_t } from "~/lib/_plugin-vue_export-helper.js";
|
|
import { n as sanitize_n } from "~/lib/sanitize.js";
|
|
var g = [`aria-label`],
|
|
_ = [`title`, `aria-label`, `disabled`, `onClick`],
|
|
v = [
|
|
`id`,
|
|
`aria-label`,
|
|
`aria-disabled`,
|
|
`contenteditable`,
|
|
`data-placeholder`,
|
|
],
|
|
y = Object.assign(
|
|
_plugin_vue_export_helper_t(
|
|
defineComponent({
|
|
__name: `RichTextEditor`,
|
|
props: mergeModels(
|
|
{
|
|
id: {},
|
|
label: {
|
|
default: `正文`,
|
|
},
|
|
placeholder: {
|
|
default: `输入内容`,
|
|
},
|
|
disabled: {
|
|
type: Boolean,
|
|
},
|
|
minHeight: {
|
|
default: 240,
|
|
},
|
|
},
|
|
{
|
|
modelValue: {
|
|
default: ``,
|
|
},
|
|
modelModifiers: {},
|
|
},
|
|
),
|
|
emits: [`update:modelValue`],
|
|
setup(r) {
|
|
let u = r,
|
|
m = useModel(r, `modelValue`),
|
|
y = ref(null),
|
|
b = null,
|
|
x = [
|
|
{
|
|
name: `加粗`,
|
|
command: `bold`,
|
|
icon: `pi pi-bold`,
|
|
},
|
|
{
|
|
name: `斜体`,
|
|
command: `italic`,
|
|
icon: `pi pi-italic`,
|
|
},
|
|
{
|
|
name: `下划线`,
|
|
command: `underline`,
|
|
icon: `pi pi-underline`,
|
|
},
|
|
{
|
|
name: `有序列表`,
|
|
command: `insertOrderedList`,
|
|
icon: `pi pi-list`,
|
|
},
|
|
{
|
|
name: `无序列表`,
|
|
command: `insertUnorderedList`,
|
|
icon: `pi pi-list-check`,
|
|
},
|
|
{
|
|
name: `左对齐`,
|
|
command: `justifyLeft`,
|
|
icon: `pi pi-align-left`,
|
|
},
|
|
{
|
|
name: `居中对齐`,
|
|
command: `justifyCenter`,
|
|
icon: `pi pi-align-center`,
|
|
},
|
|
{
|
|
name: `右对齐`,
|
|
command: `justifyRight`,
|
|
icon: `pi pi-align-right`,
|
|
},
|
|
];
|
|
// 仅保存编辑区域内的选区,点击工具栏后可恢复到用户原来的光标位置。
|
|
function S() {
|
|
let e = window.getSelection();
|
|
e?.rangeCount &&
|
|
y.value?.contains(e.anchorNode) &&
|
|
(b = e.getRangeAt(0).cloneRange());
|
|
}
|
|
// 空白正文输出空串,其余内容先清洗再同步 v-model,并更新当前选区。
|
|
function C() {
|
|
y.value &&
|
|
((m.value = y.value.innerText.trim()
|
|
? sanitize_n(y.value.innerHTML)
|
|
: ``),
|
|
S());
|
|
}
|
|
function w(e) {
|
|
if (!u.disabled && y.value) {
|
|
if (
|
|
(y.value.focus(),
|
|
b && y.value.contains(b.commonAncestorContainer))
|
|
) {
|
|
let e = window.getSelection();
|
|
(e?.removeAllRanges(), e?.addRange(b));
|
|
}
|
|
(document.execCommand(e, !1), C());
|
|
}
|
|
}
|
|
// 粘贴只接收纯文本,阻止剪贴板 HTML 直接带入额外标签和属性。
|
|
function T(e) {
|
|
(e.preventDefault(),
|
|
!u.disabled &&
|
|
(document.execCommand(
|
|
`insertText`,
|
|
!1,
|
|
e.clipboardData?.getData(`text/plain`) || ``,
|
|
),
|
|
C()));
|
|
}
|
|
return (
|
|
onMounted(() => {
|
|
y.value && (y.value.innerHTML = sanitize_n(m.value));
|
|
}),
|
|
watch(m, (e) => {
|
|
y.value &&
|
|
sanitize_n(y.value.innerHTML) !== e &&
|
|
(y.value.innerHTML = sanitize_n(e));
|
|
}),
|
|
(t, i) => (
|
|
openBlock(),
|
|
createElementBlock(
|
|
`div`,
|
|
{
|
|
class: normalizeClass([
|
|
`rich-editor`,
|
|
{
|
|
disabled: r.disabled,
|
|
},
|
|
]),
|
|
},
|
|
[
|
|
createElementVNode(
|
|
`div`,
|
|
{
|
|
class: `editor-toolbar`,
|
|
role: `toolbar`,
|
|
"aria-label": `${r.label}格式`,
|
|
},
|
|
[
|
|
(openBlock(),
|
|
createElementBlock(
|
|
Fragment,
|
|
null,
|
|
renderList(x, (e) =>
|
|
createElementVNode(
|
|
`button`,
|
|
{
|
|
key: e.command,
|
|
type: `button`,
|
|
title: e.name,
|
|
"aria-label": e.name,
|
|
disabled: r.disabled,
|
|
onMousedown: (i[0] ||= withModifiers(() => {}, [
|
|
`prevent`,
|
|
])),
|
|
onClick: (t) => w(e.command),
|
|
},
|
|
[
|
|
createElementVNode(
|
|
`i`,
|
|
{
|
|
class: normalizeClass(e.icon),
|
|
"aria-hidden": `true`,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
],
|
|
40,
|
|
_,
|
|
),
|
|
),
|
|
64,
|
|
)),
|
|
],
|
|
8,
|
|
g,
|
|
),
|
|
createElementVNode(
|
|
`div`,
|
|
{
|
|
id: r.id,
|
|
ref_key: `editor`,
|
|
ref: y,
|
|
class: `editor-content`,
|
|
role: `textbox`,
|
|
"aria-label": r.label,
|
|
"aria-multiline": `true`,
|
|
"aria-disabled": r.disabled,
|
|
contenteditable: !r.disabled,
|
|
"data-placeholder": r.placeholder,
|
|
style: normalizeStyle({
|
|
minHeight: `${r.minHeight}px`,
|
|
}),
|
|
onInput: C,
|
|
onKeyup: S,
|
|
onMouseup: S,
|
|
onBlur: S,
|
|
onPaste: T,
|
|
},
|
|
null,
|
|
44,
|
|
v,
|
|
),
|
|
],
|
|
2,
|
|
)
|
|
)
|
|
);
|
|
},
|
|
}),
|
|
[[`__scopeId`, `data-v-858a2ddd`]],
|
|
),
|
|
{
|
|
__name: `RichTextEditor`,
|
|
},
|
|
);
|
|
export {};
|
|
export default y;
|
|
</script>
|