conflict-nuxt-4/app/components/auto-import/TiptapEditor.vue
2026-02-14 14:28:25 +03:30

708 lines
16 KiB
Vue
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<!-- تولبار ادیتور -->
<div class="editor-toolbar" v-if="editor">
<div
class="toolbar-group"
v-for="(group, gIndex) in toolbarGroups"
:key="gIndex"
>
<button
v-for="(btn, index) in group"
:key="index"
@click="btn.action(editor)"
:class="{ 'is-active': btn.isActive ? btn.isActive(editor) : false }"
:disabled="btn.disabled ? !btn.disabled(editor) : false"
:title="btn.title"
>
<span class="toolbar-icon">{{ btn.icon }}</span>
</button>
</div>
</div>
<!-- محتوای ادیتور -->
<editor-content :editor="editor" class="editor-content" />
</div>
</template>
<script setup>
import { ref, nextTick, onMounted, onBeforeUnmount, watch } from "vue";
import {
Details,
DetailsContent,
DetailsSummary,
} from "@tiptap/extension-details";
import { Placeholder } from "@tiptap/extensions";
import StarterKit from "@tiptap/starter-kit";
import Underline from "@tiptap/extension-underline";
import { Editor, EditorContent } from "@tiptap/vue-3";
const props = defineProps({
accordionData: {
type: Array,
default: () => [],
},
});
const editor = ref(null);
function isList(text) {
const lines = text.split("\n").filter((line) => line.trim() !== "");
return lines.some(
(line) =>
line.trim().startsWith("*") ||
line.trim().startsWith("-") ||
/^\d+\./.test(line.trim()),
);
}
const toolbarGroups = [
// گروه فرمت متن
[
{
title: "بولد",
icon: "𝐁",
action: (editor) => editor.chain().focus().toggleBold().run(),
isActive: (editor) => editor.isActive("bold"),
},
{
title: "ایتالیک",
icon: "𝐼",
action: (editor) => editor.chain().focus().toggleItalic().run(),
isActive: (editor) => editor.isActive("italic"),
},
{
title: "زیرخط",
icon: "𝑈",
action: (editor) => editor.chain().focus().toggleUnderline().run(),
isActive: (editor) => editor.isActive("underline"),
},
{
title: "خط خورده",
icon: "̶S̶",
action: (editor) => editor.chain().focus().toggleStrike().run(),
isActive: (editor) => editor.isActive("strike"),
},
],
// گروه هدینگ‌ها
[
{
title: "عنوان ۱",
icon: "H1",
action: (editor) =>
editor.chain().focus().toggleHeading({ level: 1 }).run(),
isActive: (editor) => editor.isActive("heading", { level: 1 }),
},
{
title: "عنوان ۲",
icon: "H2",
action: (editor) =>
editor.chain().focus().toggleHeading({ level: 2 }).run(),
isActive: (editor) => editor.isActive("heading", { level: 2 }),
},
{
title: "عنوان ۳",
icon: "H3",
action: (editor) =>
editor.chain().focus().toggleHeading({ level: 3 }).run(),
isActive: (editor) => editor.isActive("heading", { level: 3 }),
},
],
// گروه لیست و نقل قول
[
{
title: "لیست بولت‌دار",
icon: "•",
action: (editor) => editor.chain().focus().toggleBulletList().run(),
isActive: (editor) => editor.isActive("bulletList"),
},
{
title: "لیست شماره‌دار",
icon: "1.",
action: (editor) => editor.chain().focus().toggleOrderedList().run(),
isActive: (editor) => editor.isActive("orderedList"),
},
{
title: "نقل قول",
icon: '"',
action: (editor) => editor.chain().focus().toggleBlockquote().run(),
isActive: (editor) => editor.isActive("blockquote"),
},
{
title: "کد",
icon: "{ }",
action: (editor) => editor.chain().focus().toggleCodeBlock().run(),
isActive: (editor) => editor.isActive("codeBlock"),
},
],
// گروه جزییات
[
{
title: "افزودن جزییات",
icon: "📋",
action: (editor) => editor.chain().focus().setDetails().run(),
disabled: (editor) => !editor.can().setDetails(),
},
{
title: "حذف جزییات",
icon: "🗑️",
action: (editor) => editor.chain().focus().unsetDetails().run(),
disabled: (editor) => !editor.can().unsetDetails(),
},
],
// گروه Undo/Redo
[
{
title: "بازگشت",
icon: "↩",
action: (editor) => editor.chain().focus().undo().run(),
disabled: (editor) => !editor.can().undo(),
},
{
title: "جلو",
icon: "↪",
action: (editor) => editor.chain().focus().redo().run(),
disabled: (editor) => !editor.can().redo(),
},
],
];
function formatListToHtml(text) {
const lines = text.split("\n").filter((line) => line.trim() !== "");
let inList = false;
let listType = "";
let html = "";
lines.forEach((line) => {
const trimmedLine = line.trim();
if (trimmedLine.startsWith("*") || trimmedLine.startsWith("-")) {
if (!inList || listType !== "ul") {
if (inList) html += `</${listType}>`;
listType = "ul";
html += "<ul>";
inList = true;
}
const cleanLine = trimmedLine.replace(/^[*\-]\s*/, "");
html += `<li>${escapeHtml(cleanLine)}</li>`;
} else if (/^\d+\./.test(trimmedLine)) {
if (!inList || listType !== "ol") {
if (inList) html += `</${listType}>`;
listType = "ol";
html += "<ol>";
inList = true;
}
const cleanLine = trimmedLine.replace(/^\d+\.\s*/, "");
html += `<li>${escapeHtml(cleanLine)}</li>`;
} else {
if (inList) {
html += `</${listType}>`;
inList = false;
}
html += `<p>${escapeHtml(trimmedLine)}</p>`;
}
});
if (inList) {
html += `</${listType}>`;
}
return html;
}
function formatMultilineToHtml(text) {
const lines = text.split("\n").filter((line) => line.trim() !== "");
return lines.map((line) => `<p>${escapeHtml(line)}</p>`).join("");
}
function escapeHtml(text) {
if (!text) return "";
const map = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#039;",
"`": "&#96;",
"=": "&#61;",
"/": "&#47;",
"(": "&#40;",
")": "&#41;",
};
return String(text).replace(/[&<>"'`=\/()]/g, (m) => map[m]);
}
function formatContent(content) {
if (!content) return "<p></p>";
if (isList(content)) {
return formatListToHtml(content);
}
if (content.includes("\n")) {
return formatMultilineToHtml(content);
}
return `<p>${escapeHtml(content)}</p>`;
}
function generateDetailsHtml(item) {
let html = `<details class="custom-details" ${item.isOpen ? "open" : ""}>`;
html += `<summary>`;
if (item.tag) {
html += `<span class="tag" > ${item.tag} </span>`;
}
html += `${item.title || "بدون عنوان"}`;
if (item.link_url) {
html += `<a class="link-url" href="${item.link_url}">${item.link_label}</a>`;
}
html += "</summary>";
const formattedContent = formatContent(item.content || "");
html += `<div class="details-content">${formattedContent}</div>`;
if (item.children && item.children.length > 0) {
html += '<div class="children-container">';
item.children.forEach((child) => {
html += generateDetailsHtml(child);
});
html += "</div>";
}
html += `</details>`;
return html;
}
function loadFromJson() {
if (!editor.value) return;
let html =
'<p style="margin-bottom: 1.5rem; color: var(--color-dark-primary-700);"></p>';
props.accordionData.forEach((item) => {
html += generateDetailsHtml(item);
});
editor.value.commands.setContent(html);
}
watch(
() => props.accordionData,
(newVal) => {
if (!editor.value) return;
if (!newVal || newVal.length === 0) return;
console.log("accordionData updated:", newVal);
nextTick(() => {
loadFromJson();
});
},
{ deep: true, immediate: true },
);
onMounted(() => {
editor.value = new Editor({
extensions: [
StarterKit,
Underline,
Details.configure({
persist: true,
HTMLAttributes: {
class: "custom-details",
},
}),
DetailsSummary,
DetailsContent,
Placeholder.configure({
includeChildren: true,
placeholder: ({ node }) => {
if (node.type.name === "detailsSummary") {
return "عنوان جزییات را وارد کنید...";
}
return null;
},
}),
],
content: `<p style="margin-bottom: 1.5rem; color: var(--color-dark-primary-700);">✨ در حال بارگذاری...</p>`,
});
nextTick(() => {
loadFromJson();
});
});
onBeforeUnmount(() => {
if (editor.value) {
editor.value.destroy();
}
});
</script>
<style lang="scss">
:root {
/* فقط متغیرهای غیررنگ (سایه، انحنا، RGB اصلی) رنگ‌ها در main.css تعریف شده‌اند */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--shadow-md:
0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--radius: 0.75rem;
--radius-sm: 0.5rem;
--radius-lg: 1rem;
--color-primary-rgb: 0, 186, 186; /* مقدار RGB #00baba (primary-500) */
}
/* تولبار ادیتور */
.editor-toolbar {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
padding: 0.75rem 1.25rem;
background: linear-gradient(
135deg,
var(--color-primary-50) 0%,
var(--color-primary-100) 100%
);
border: 1px solid var(--color-primary-200);
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
margin: 2rem auto 0;
max-width: 1200px;
box-shadow: var(--shadow-sm);
}
.toolbar-group {
display: flex;
gap: 0.25rem;
padding: 0.25rem;
background: var(--color-white-normal);
border-radius: var(--radius-sm);
border: 1px solid var(--color-dark-primary-200);
button {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.25rem;
height: 2.25rem;
padding: 0 0.5rem;
background: transparent;
border: none;
border-radius: var(--radius-sm);
color: var(--color-dark-primary-600);
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: var(--color-primary-100);
color: var(--color-primary-700);
}
&.is-active {
background: var(--color-primary);
color: var(--color-white-normal);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
&:hover {
background: transparent;
color: var(--color-dark-primary-400);
}
}
.toolbar-icon {
font-size: 1.1rem;
font-weight: 600;
}
}
}
.editor-content {
max-width: 1200px;
margin: 0 auto 2rem;
padding: 2rem;
background: var(--color-white-normal);
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
box-shadow: var(--shadow-md);
min-height: 500px;
border: 1px solid var(--color-dark-primary-200);
border-top: none;
}
.tiptap {
font-family:
"Vazir",
"Inter",
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
color: var(--color-dark-primary-700);
line-height: 1.7;
&:focus {
outline: none;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 1.5rem 0 0.75rem;
color: var(--color-dark-primary-800);
font-weight: 700;
}
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1.25rem;
}
ul,
ol {
padding-right: 1.5rem;
margin: 0.75rem 0;
}
li {
margin: 0.25rem 0;
}
blockquote {
border-right: 4px solid var(--color-primary);
padding: 0.75rem 1.25rem;
margin: 1rem 0;
background: var(--color-dark-primary-50);
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
color: var(--color-dark-primary-600);
font-style: italic;
}
pre {
background: var(--color-dark-primary-800);
color: var(--color-dark-primary-100);
padding: 1rem;
border-radius: var(--radius-sm);
overflow-x: auto;
font-family: "Courier New", monospace;
font-size: 0.9rem;
}
code {
background: var(--color-dark-primary-100);
color: var(--color-primary-700);
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-family: "Courier New", monospace;
font-size: 0.9rem;
}
}
/* استایل آکاردئون‌ها */
.custom-details {
position: relative;
background: var(--color-white-normal);
border: 1px solid var(--color-dark-primary-200);
border-radius: 0.25em;
margin: 1.25rem 0;
transition: all 0.2s ease;
box-shadow: var(--shadow-sm);
&:hover {
border-color: var(--color-primary-300);
box-shadow: var(--shadow);
}
> button {
position: absolute;
left: 2rem;
top: 3rem;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-primary-100);
border: 1px solid var(--color-primary-300);
border-radius: var(--radius-sm);
color: var(--color-primary-700);
cursor: pointer;
transition: all 0.2s ease;
z-index: 10;
&:hover {
background: var(--color-primary);
color: var(--color-white-normal);
transform: scale(1.05);
}
&::before {
content: "▼";
font-size: 0.75rem;
transition: transform 0.2s ease;
}
}
summary.tag {
color: blue;
border-radius: var(--radius) var(--radius) 0 0;
}
summary.label {
color: chocolate;
font-weight: 700;
}
summary {
position: relative;
padding: 1.25rem 1.25rem 1.25rem 3.5rem;
background: linear-gradient(
to right,
var(--color-primary-100),
var(--color-white-normal)
);
color: var(--color-primary-700);
border-radius: var(--radius) var(--radius) 0 0;
font-size: 1.1rem;
color: var(--color-dark-primary-800);
// cursor: pointer;
list-style: none;
user-select: none;
transition: all 0.2s ease;
&::-webkit-details-marker {
display: none;
}
}
> div {
padding: 1.5rem;
background: linear-gradient(
to bottom,
var(--color-white-normal),
var(--color-dark-primary-50)
);
// border-radius: 0 0 var(--radius) var(--radius);
animation: slideDown 0.25s ease-out;
p {
margin: 0.5rem 0;
color: var(--color-dark-primary-600);
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
ul,
ol {
margin: 0.5rem 0;
padding-right: 1.5rem;
li {
margin: 0.25rem 0;
color: var(--color-dark-primary-600);
&::marker {
color: var(--color-primary);
}
}
}
}
&.is-open {
border-color: var(--color-primary);
box-shadow: 0 4px 12px rgba(var(--color-primary-rgb), 0.1);
> button {
background: var(--color-primary);
color: var(--color-white-normal);
&::before {
transform: rotate(180deg);
}
}
> summary {
border-bottom: 2px solid var(--color-primary-100);
color: var(--color-primary-700);
}
}
.children-container {
margin-top: 1rem;
padding-right: 1rem;
border-right: 2px dashed var(--color-primary-100);
}
.link-url {
color: blue;
margin-right: 0.5em;
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ریسپانسیو */
@media (max-width: 768px) {
.editor-toolbar {
margin: 1rem 1rem 0;
padding: 0.5rem;
border-radius: var(--radius);
}
.toolbar-group {
flex-wrap: wrap;
button {
min-width: 2rem;
height: 2rem;
}
}
.editor-content {
margin: 0 1rem 1rem;
padding: 1.25rem;
border-radius: var(--radius);
}
.custom-details {
summary {
padding: 1rem 1rem 1rem 3rem;
font-size: 1rem;
}
> button {
left: 0.75rem;
top: 1rem;
width: 1.75rem;
height: 1.75rem;
}
}
}
</style>