2039 lines
49 KiB
Vue
Executable File
2039 lines
49 KiB
Vue
Executable File
<template>
|
||
<div class="editor-shell container pt-0">
|
||
<!-- نوار ابزار ثابت در بالای صفحه -->
|
||
<div v-if="editor" class="menu-bar-sticky">
|
||
<div class="menu-bar">
|
||
<!-- نوار ابزار یک خطی -->
|
||
<div class="toolbar-container">
|
||
<!-- بخش اول: متن -->
|
||
<div class="toolbar-section">
|
||
<button
|
||
@click="handleToolbarButtonClick({ action: 'toggleBold' })"
|
||
:class="{ 'is-active': editor.isActive('bold') }"
|
||
class="toolbar-btn"
|
||
title="پررنگ (Ctrl+B)"
|
||
>
|
||
<UIcon :name="'lucide-bold'" />
|
||
</button>
|
||
<button
|
||
@click="handleToolbarButtonClick({ action: 'toggleItalic' })"
|
||
:class="{ 'is-active': editor.isActive('italic') }"
|
||
class="toolbar-btn"
|
||
title="کج (Ctrl+I)"
|
||
>
|
||
<UIcon :name="'lucide-italic'" />
|
||
</button>
|
||
<button
|
||
@click="handleToolbarButtonClick({ action: 'toggleUnderline' })"
|
||
:class="{ 'is-active': editor.isActive('underline') }"
|
||
class="toolbar-btn"
|
||
title="زیرخط (Ctrl+U)"
|
||
>
|
||
<UIcon :name="'lucide-underline'" />
|
||
</button>
|
||
<button
|
||
@click="handleToolbarButtonClick({ action: 'toggleStrike' })"
|
||
:class="{ 'is-active': editor.isActive('strike') }"
|
||
class="toolbar-btn"
|
||
title="خط خورده (Ctrl+Shift+S)"
|
||
>
|
||
<UIcon :name="'lucide-strikethrough'" />
|
||
</button>
|
||
|
||
<!-- انتخاب رنگ متن -->
|
||
<div class="color-picker-wrapper">
|
||
<button class="toolbar-btn" title="رنگ متن">
|
||
<UIcon :name="'lucide-palette'" />
|
||
</button>
|
||
<input
|
||
type="color"
|
||
class="color-picker"
|
||
@change="setTextColor($event)"
|
||
/>
|
||
</div>
|
||
|
||
<!-- انتخاب سایز فونت -->
|
||
<select
|
||
class="font-size-select"
|
||
@change="setFontSize($event)"
|
||
title="سایز فونت"
|
||
>
|
||
<option value="12">12</option>
|
||
<option value="14" selected>14</option>
|
||
<option value="16">16</option>
|
||
<option value="18">18</option>
|
||
<option value="20">20</option>
|
||
<option value="24">24</option>
|
||
<option value="28">28</option>
|
||
<option value="32">32</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- جداکننده -->
|
||
<div class="toolbar-divider"></div>
|
||
|
||
<!-- بخش دوم: تراز و استایل -->
|
||
<div class="toolbar-section">
|
||
<button
|
||
@click="
|
||
handleToolbarButtonClick({
|
||
action: 'setTextAlign',
|
||
value: 'right',
|
||
})
|
||
"
|
||
:class="{ 'is-active': editor.isActive({ textAlign: 'right' }) }"
|
||
class="toolbar-btn"
|
||
title="تراز به راست"
|
||
>
|
||
<UIcon :name="'lucide-align-right'" />
|
||
</button>
|
||
<button
|
||
@click="
|
||
handleToolbarButtonClick({
|
||
action: 'setTextAlign',
|
||
value: 'center',
|
||
})
|
||
"
|
||
:class="{ 'is-active': editor.isActive({ textAlign: 'center' }) }"
|
||
class="toolbar-btn"
|
||
title="تراز به وسط"
|
||
>
|
||
<UIcon :name="'lucide-align-center'" />
|
||
</button>
|
||
<button
|
||
@click="
|
||
handleToolbarButtonClick({
|
||
action: 'setTextAlign',
|
||
value: 'left',
|
||
})
|
||
"
|
||
:class="{ 'is-active': editor.isActive({ textAlign: 'left' }) }"
|
||
class="toolbar-btn"
|
||
title="تراز به چپ"
|
||
>
|
||
<UIcon :name="'lucide-align-left'" />
|
||
</button>
|
||
<button
|
||
@click="handleToolbarButtonClick({ action: 'toggleBulletList' })"
|
||
:class="{ 'is-active': editor.isActive('bulletList') }"
|
||
class="toolbar-btn"
|
||
title="لیست نقطهای"
|
||
>
|
||
<UIcon :name="'lucide-list'" />
|
||
</button>
|
||
<button
|
||
@click="handleToolbarButtonClick({ action: 'toggleOrderedList' })"
|
||
:class="{ 'is-active': editor.isActive('orderedList') }"
|
||
class="toolbar-btn"
|
||
title="لیست شمارهای"
|
||
>
|
||
<UIcon :name="'lucide-list-check'" />
|
||
</button>
|
||
</div>
|
||
|
||
<!-- جداکننده -->
|
||
<div class="toolbar-divider"></div>
|
||
|
||
<!-- بخش سوم: سرتیترها -->
|
||
<div class="toolbar-section">
|
||
<select
|
||
class="heading-select"
|
||
@change="setHeading($event)"
|
||
title="سرفصل"
|
||
>
|
||
<option value="paragraph">پاراگراف</option>
|
||
<option value="h1">h1</option>
|
||
<option value="h2">h2</option>
|
||
<option value="h3">h3</option>
|
||
<option value="h4">h4</option>
|
||
</select>
|
||
|
||
<button
|
||
@click="handleToolbarButtonClick({ action: 'toggleBlockquote' })"
|
||
:class="{ 'is-active': editor.isActive('blockquote') }"
|
||
class="toolbar-btn"
|
||
title="نقل قول"
|
||
>
|
||
<UIcon :name="'lucide-quote'" />
|
||
</button>
|
||
|
||
<button
|
||
@click="handleToolbarButtonClick({ action: 'toggleCodeBlock' })"
|
||
:class="{ 'is-active': editor.isActive('codeBlock') }"
|
||
class="toolbar-btn"
|
||
title="کد"
|
||
>
|
||
<UIcon :name="'lucide-code'" />
|
||
</button>
|
||
</div>
|
||
|
||
<!-- جداکننده -->
|
||
<div class="toolbar-divider"></div>
|
||
|
||
<!-- بخش چهارم: ابزارهای پیشرفته -->
|
||
<div class="toolbar-section">
|
||
<button
|
||
@click="toggleLink"
|
||
:class="{ 'is-active': editor.isActive('link') }"
|
||
class="toolbar-btn"
|
||
title="لینک (Ctrl+K)"
|
||
>
|
||
<UIcon :name="'lucide-link'" />
|
||
</button>
|
||
|
||
<button
|
||
@click="handleToolbarButtonClick({ action: 'setHorizontalRule' })"
|
||
class="toolbar-btn"
|
||
title="خط جداکننده"
|
||
>
|
||
<UIcon :name="'lucide-minus'" />
|
||
</button>
|
||
|
||
<button
|
||
@click="handleToolbarButtonClick({ action: 'undo' })"
|
||
:disabled="!editor.can().undo()"
|
||
class="toolbar-btn"
|
||
title="بازگردانی (Ctrl+Z)"
|
||
>
|
||
<UIcon :name="'lucide-rotate-ccw'" />
|
||
</button>
|
||
|
||
<button
|
||
@click="handleToolbarButtonClick({ action: 'redo' })"
|
||
:disabled="!editor.can().redo()"
|
||
class="toolbar-btn"
|
||
title="انجام مجدد (Ctrl+Y)"
|
||
>
|
||
<UIcon :name="'lucide-rotate-cw'" />
|
||
</button>
|
||
|
||
<!-- منوی بیشتر -->
|
||
<div class="more-menu-wrapper">
|
||
<button class="toolbar-btn more-btn" title="ابزارهای بیشتر">
|
||
<UIcon :name="'lucide-more-horizontal'" />
|
||
</button>
|
||
<div class="more-dropdown">
|
||
<button
|
||
@click="
|
||
handleToolbarButtonClick({ action: 'toggleTaskList' })
|
||
"
|
||
:class="{ 'is-active': editor.isActive('taskList') }"
|
||
class="dropdown-item"
|
||
>
|
||
<UIcon :name="'lucide-list-check'" />
|
||
<span>لیست کارها</span>
|
||
</button>
|
||
<button class="dropdown-item" @click="clearFormatting">
|
||
<UIcon :name="'lucide-eraser'" />
|
||
<span>پاک کردن فرمت</span>
|
||
</button>
|
||
<button class="dropdown-item" @click="copyToClipboard">
|
||
<UIcon :name="'lucide-copy'" />
|
||
<span>کپی با فرمت</span>
|
||
</button>
|
||
<div class="dropdown-divider"></div>
|
||
<button class="dropdown-item" @click="exportAsHTML">
|
||
<UIcon :name="'lucide-download'" />
|
||
<span>خروجی HTML</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- محتوای ویرایشگر با Context Menu -->
|
||
<UContextMenu
|
||
:items="contextMenuItems"
|
||
:ui="{
|
||
content: 'w-64',
|
||
itemTrailingIcon: 'hidden',
|
||
}"
|
||
dir="rtl"
|
||
@select="handleContextMenuSelect"
|
||
>
|
||
<!-- Custom slot برای آیکون چپ چرخان -->
|
||
<template #item-trailing="{ item }">
|
||
<UIcon
|
||
v-if="item.children"
|
||
name="i-lucide-chevron-left"
|
||
class="shrink-0 size-5 ms-auto"
|
||
:class="{
|
||
'rotate-180': !$dir === 'rtl',
|
||
}"
|
||
/>
|
||
<div v-else-if="item.kbds" class="flex gap-1 ms-auto">
|
||
<UKbd v-for="(kbd, index) in item.kbds" :key="index" :value="kbd" />
|
||
</div>
|
||
</template>
|
||
<div class="editor-content-wrapper" @mouseup="handleTextSelection">
|
||
<EditorContent :editor="editor" />
|
||
|
||
<!-- آیکونهای بلوک (شبیه Notion) -->
|
||
<!-- <div
|
||
v-for="(block, index) in blocks"
|
||
:key="block.id"
|
||
class="block-icon-wrapper"
|
||
:style="getBlockIconStyle(block, index)"
|
||
@click="showBlockMenuAction(block, $event)"
|
||
>
|
||
<div class="block-icon">
|
||
{{ getBlockIcon(block.type) }}
|
||
</div>
|
||
</div> -->
|
||
</div>
|
||
</UContextMenu>
|
||
|
||
<!-- منوی بلوک -->
|
||
<BlockMenu
|
||
v-if="showBlockMenu"
|
||
:visible="showBlockMenu"
|
||
:position="blockMenuPosition"
|
||
:block-type="currentBlockType"
|
||
:block-data="currentBlockData"
|
||
@close="showBlockMenu = false"
|
||
@action="handleBlockMenuAction"
|
||
/>
|
||
|
||
<!-- مودال برای لینک -->
|
||
<div v-if="showLinkModal" class="modal-overlay" @click="closeLinkModal">
|
||
<div class="modal" @click.stop>
|
||
<div class="modal-header">
|
||
<h3>افزودن لینک</h3>
|
||
<button @click="closeLinkModal" class="close-btn">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<input
|
||
v-model="linkUrl"
|
||
type="url"
|
||
placeholder="https://example.com"
|
||
class="link-input"
|
||
@keyup.enter="setLink"
|
||
ref="linkInput"
|
||
/>
|
||
<div class="link-example">مثال: https://google.com</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button @click="setLink" class="btn-primary">افزودن لینک</button>
|
||
<button
|
||
v-if="editor && editor.isActive('link')"
|
||
@click="unsetLink"
|
||
class="btn-danger"
|
||
>
|
||
حذف لینک
|
||
</button>
|
||
<button @click="closeLinkModal" class="btn-secondary">انصراف</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- وضعیت پردازش AI -->
|
||
<div v-if="isAIProcessing" class="ai-processing-overlay">
|
||
<div class="ai-processing">
|
||
<div class="flex justify-center">
|
||
<img src="/robot.png" alt="" class="h-32 w-32 animate-pulse-glow" />
|
||
</div>
|
||
<div class="ai-text">در حال پردازش توسط هوش مصنوعی...</div>
|
||
<div class="ai-progress"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { EditorContent, useEditor } from "@tiptap/vue-3";
|
||
import StarterKit from "@tiptap/starter-kit";
|
||
import Placeholder from "@tiptap/extension-placeholder";
|
||
import Link from "@tiptap/extension-link";
|
||
import TaskList from "@tiptap/extension-task-list";
|
||
import TaskItem from "@tiptap/extension-task-item";
|
||
import HorizontalRule from "@tiptap/extension-horizontal-rule";
|
||
import TextAlign from "@tiptap/extension-text-align";
|
||
import DragHandle from "@tiptap/extension-drag-handle";
|
||
|
||
import { usePageStore } from "@/stores/page";
|
||
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from "vue";
|
||
import EditorSchema from "@/json/EditorSchema.json";
|
||
|
||
const pageStore = usePageStore();
|
||
|
||
// وضعیتها
|
||
const showLinkModal = ref(false);
|
||
const linkUrl = ref("");
|
||
const lastSavedTime = ref("");
|
||
const linkInput = ref(null);
|
||
|
||
// منوها
|
||
const showBlockMenu = ref(false);
|
||
const blockMenuPosition = ref({ x: 0, y: 0 });
|
||
const selectedText = ref("");
|
||
const currentBlockType = ref("paragraph");
|
||
const currentBlockData = ref({});
|
||
const isAIProcessing = ref(false);
|
||
|
||
// دادههای Schema
|
||
const toolbarSchema = EditorSchema.toolbarButtons;
|
||
const contextMenuSchema = EditorSchema.contextMenuItems;
|
||
const blockIcons = EditorSchema.blockIcons;
|
||
const blockTypeNames = EditorSchema.blockTypeNames;
|
||
const alignmentNames = EditorSchema.alignmentNames;
|
||
const editorConfig = EditorSchema.editorConfig;
|
||
|
||
// Context Menu Items برای Nuxt UI
|
||
const contextMenuItems = computed(() => {
|
||
const buildMenuItems = (items) => {
|
||
return items.map((item) => {
|
||
const menuItem = {
|
||
label: item.label,
|
||
icon: item.icon,
|
||
kbds: item.kbds,
|
||
color: item.color,
|
||
};
|
||
|
||
// اگر آیتم children داشته باشد (مانند ترجمه)
|
||
if (item.children && item.children.length > 0) {
|
||
menuItem.children = item.children.map((childRow) =>
|
||
childRow.map((child) => ({
|
||
label: child.label,
|
||
icon: child.icon,
|
||
kbds: child.kbds,
|
||
color: child.color,
|
||
// ذخیره دادههای اضافی مانند lang
|
||
meta: {
|
||
action: child.action,
|
||
lang: child.lang,
|
||
},
|
||
onSelect: () =>
|
||
handleContextMenuItemClick(child.action, child.lang),
|
||
})),
|
||
);
|
||
} else {
|
||
// آیتمهای عادی بدون children
|
||
menuItem.onSelect = () => handleContextMenuItemClick(item.action);
|
||
}
|
||
|
||
return menuItem;
|
||
});
|
||
};
|
||
|
||
const sections = [];
|
||
|
||
contextMenuSchema.forEach((section) => {
|
||
const sectionChildren = [];
|
||
|
||
section.items.forEach((row) => {
|
||
const rowItems = buildMenuItems(row);
|
||
sectionChildren.push(rowItems);
|
||
});
|
||
|
||
sections.push([
|
||
{
|
||
label: section.label,
|
||
icon: section.icon,
|
||
children: sectionChildren,
|
||
},
|
||
]);
|
||
});
|
||
|
||
return sections;
|
||
});
|
||
|
||
// ==================== توابع جدید برای نوار ابزار یک خطی ====================
|
||
|
||
// تنظیم سرتیتر
|
||
const setHeading = (event) => {
|
||
const value = event.target.value;
|
||
if (!editor.value) return;
|
||
|
||
switch (value) {
|
||
case "h1":
|
||
editor.value.chain().focus().toggleHeading({ level: 1 }).run();
|
||
break;
|
||
case "h2":
|
||
editor.value.chain().focus().toggleHeading({ level: 2 }).run();
|
||
break;
|
||
case "h3":
|
||
editor.value.chain().focus().toggleHeading({ level: 3 }).run();
|
||
break;
|
||
case "h4":
|
||
editor.value.chain().focus().toggleHeading({ level: 4 }).run();
|
||
break;
|
||
default:
|
||
editor.value.chain().focus().setParagraph().run();
|
||
}
|
||
};
|
||
|
||
// تنظیم سایز فونت
|
||
const setFontSize = (event) => {
|
||
const size = event.target.value;
|
||
if (!editor.value) return;
|
||
|
||
// اگر extension textStyle نصب شده باشد
|
||
editor.value
|
||
.chain()
|
||
.focus()
|
||
.setMark("textStyle", { fontSize: `${size}px` })
|
||
.run();
|
||
};
|
||
|
||
// تنظیم رنگ متن
|
||
const setTextColor = (event) => {
|
||
const color = event.target.value;
|
||
if (!editor.value) return;
|
||
|
||
editor.value.chain().focus().setColor(color).run();
|
||
};
|
||
|
||
// پاک کردن فرمت
|
||
const clearFormatting = () => {
|
||
if (!editor.value) return;
|
||
|
||
editor.value.chain().focus().clearNodes().unsetAllMarks().run();
|
||
};
|
||
|
||
// کپی با فرمت
|
||
const copyToClipboard = async () => {
|
||
if (!editor.value) return;
|
||
|
||
try {
|
||
const html = editor.value.getHTML();
|
||
await navigator.clipboard.write([
|
||
new ClipboardItem({
|
||
"text/html": new Blob([html], { type: "text/html" }),
|
||
"text/plain": new Blob([editor.value.getText()], {
|
||
type: "text/plain",
|
||
}),
|
||
}),
|
||
]);
|
||
showToast("متن با فرمت کپی شد");
|
||
} catch (error) {
|
||
console.error("خطا در کپی:", error);
|
||
showToast("خطا در کپی کردن");
|
||
}
|
||
};
|
||
|
||
// خروجی HTML
|
||
const exportAsHTML = () => {
|
||
if (!editor.value) return;
|
||
|
||
const html = editor.value.getHTML();
|
||
const blob = new Blob([html], { type: "text/html" });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement("a");
|
||
a.href = url;
|
||
a.download = "متن-ویرایش-شده.html";
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
|
||
showToast("فایل HTML ذخیره شد");
|
||
};
|
||
|
||
// ==================== توابع مدیریت نوار ابزار ====================
|
||
|
||
// مدیریت کلیک دکمههای نوار ابزار
|
||
const handleToolbarButtonClick = (btn) => {
|
||
if (!editor.value) return;
|
||
|
||
switch (btn.action) {
|
||
case "toggleHeading":
|
||
editor.value.chain().focus().toggleHeading({ level: btn.level }).run();
|
||
break;
|
||
case "toggleBold":
|
||
editor.value.chain().focus().toggleBold().run();
|
||
break;
|
||
case "toggleItalic":
|
||
editor.value.chain().focus().toggleItalic().run();
|
||
break;
|
||
case "toggleUnderline":
|
||
editor.value.chain().focus().toggleUnderline().run();
|
||
break;
|
||
case "toggleStrike":
|
||
editor.value.chain().focus().toggleStrike().run();
|
||
break;
|
||
case "toggleLink":
|
||
toggleLink();
|
||
break;
|
||
case "toggleBulletList":
|
||
editor.value.chain().focus().toggleBulletList().run();
|
||
break;
|
||
case "toggleOrderedList":
|
||
editor.value.chain().focus().toggleOrderedList().run();
|
||
break;
|
||
case "toggleTaskList":
|
||
editor.value.chain().focus().toggleTaskList().run();
|
||
break;
|
||
case "toggleCodeBlock":
|
||
editor.value.chain().focus().toggleCodeBlock().run();
|
||
break;
|
||
case "toggleBlockquote":
|
||
editor.value.chain().focus().toggleBlockquote().run();
|
||
break;
|
||
case "setHorizontalRule":
|
||
editor.value.chain().focus().setHorizontalRule().run();
|
||
break;
|
||
case "setTextAlign":
|
||
editor.value.chain().focus().setTextAlign(btn.value).run();
|
||
break;
|
||
case "undo":
|
||
editor.value.chain().focus().undo().run();
|
||
break;
|
||
case "redo":
|
||
editor.value.chain().focus().redo().run();
|
||
break;
|
||
}
|
||
};
|
||
|
||
// ==================== توابع مدیریت Context Menu ====================
|
||
|
||
// مدیریت کلیک روی آیتمهای Context Menu
|
||
const handleContextMenuItemClick = (action, lang = null) => {
|
||
if (!editor.value) return;
|
||
|
||
console.log("Context menu clicked:", {
|
||
action,
|
||
lang,
|
||
selectedText: selectedText.value,
|
||
});
|
||
|
||
switch (action) {
|
||
case "summarize":
|
||
case "improve":
|
||
case "continue":
|
||
case "explain":
|
||
case "simplify":
|
||
case "spellcheck":
|
||
case "simplify":
|
||
case "rewrite":
|
||
case "expand":
|
||
case "title":
|
||
handleAIAction(action, selectedText.value);
|
||
break;
|
||
|
||
case "translate":
|
||
if (lang) {
|
||
handleAIAction(action, selectedText.value, lang);
|
||
} else {
|
||
// اگر lang مشخص نشده، از کاربر بپرسید
|
||
showLanguageSelectionModal();
|
||
}
|
||
break;
|
||
|
||
// ... سایر caseها مانند قبل
|
||
}
|
||
};
|
||
|
||
// مدیریت paste
|
||
const handlePaste = async () => {
|
||
try {
|
||
const clipboardText = await navigator.clipboard.readText();
|
||
editor.value?.chain().focus().insertContent(clipboardText).run();
|
||
showToast("متن چسبانده شد");
|
||
} catch (error) {
|
||
console.error("خطا در خواندن کلیپبورد:", error);
|
||
showToast("خطا در چسباندن متن");
|
||
}
|
||
};
|
||
|
||
// مدیریت انتخاب از Context Menu
|
||
const handleContextMenuSelect = (e, item) => {
|
||
console.log("Context menu item selected:", item.label);
|
||
|
||
if (item.onSelect) {
|
||
item.onSelect(e);
|
||
}
|
||
};
|
||
|
||
// ==================== ویرایشگر Tiptap ====================
|
||
|
||
const editor = useEditor({
|
||
extensions: [
|
||
StarterKit.configure({
|
||
history: false, // 👈 خیلی مهم
|
||
}),
|
||
|
||
Placeholder.configure({
|
||
placeholder: editorConfig.placeholder,
|
||
emptyEditorClass: editorConfig.emptyEditorClass,
|
||
}),
|
||
Link.configure({
|
||
openOnClick: false,
|
||
HTMLAttributes: editorConfig.linkAttributes,
|
||
}),
|
||
TaskList,
|
||
TaskItem.configure({
|
||
nested: true,
|
||
HTMLAttributes: {
|
||
class: "flex items-start gap-2",
|
||
},
|
||
}),
|
||
HorizontalRule.configure({
|
||
HTMLAttributes: {
|
||
class: "my-8",
|
||
},
|
||
}),
|
||
TextAlign.configure({
|
||
types: ["heading", "paragraph"],
|
||
alignments: ["right", "center", "left"],
|
||
defaultAlignment: editorConfig.defaultAlignment,
|
||
}),
|
||
DragHandle.configure({
|
||
draggable: true,
|
||
}),
|
||
],
|
||
content:
|
||
pageStore.blocks.length > 0
|
||
? pageStore.blocksToEditorContent()
|
||
: EditorSchema.welcomeContent,
|
||
editorProps: {
|
||
attributes: editorConfig.attributes,
|
||
handleKeyDown: (view, event) => {
|
||
// میانبرهای کیبورد
|
||
if ((event.ctrlKey || event.metaKey) && event.key === "k") {
|
||
toggleLink();
|
||
event.preventDefault();
|
||
return true;
|
||
}
|
||
if ((event.ctrlKey || event.metaKey) && event.key === "z") {
|
||
if (event.shiftKey) {
|
||
editor.commands.redo();
|
||
} else {
|
||
editor.commands.undo();
|
||
}
|
||
event.preventDefault();
|
||
return true;
|
||
}
|
||
return false;
|
||
},
|
||
},
|
||
});
|
||
|
||
// ==================== توابع مدیریت ====================
|
||
|
||
// انتخاب متن
|
||
const handleTextSelection = () => {
|
||
const selection = window.getSelection();
|
||
selectedText.value = selection.toString().trim();
|
||
};
|
||
|
||
// نمایش منوی بلوک
|
||
const showBlockMenuAction = (block, event) => {
|
||
event.stopPropagation();
|
||
|
||
currentBlockType.value = block.type;
|
||
currentBlockData.value = block.data || {};
|
||
|
||
blockMenuPosition.value = {
|
||
x: event.clientX,
|
||
y: event.clientY,
|
||
};
|
||
|
||
showBlockMenu.value = true;
|
||
|
||
console.log("🧱 منوی بلوک باز شد:", {
|
||
type: block.type,
|
||
position: blockMenuPosition.value,
|
||
data: currentBlockData.value,
|
||
});
|
||
};
|
||
|
||
// مدیریت اکشنهای منوی بلوک
|
||
const handleBlockMenuAction = ({ action, value, blockType, blockData }) => {
|
||
console.log("🎯 اکشن منوی بلوک:", action, value, blockType, blockData);
|
||
|
||
switch (action) {
|
||
case "color":
|
||
showToast(`رنگ بلوک به ${value} تغییر کرد`);
|
||
break;
|
||
case "align":
|
||
if (editor.value) {
|
||
editor.value.chain().focus().setTextAlign(value).run();
|
||
}
|
||
showToast(`تراز به ${getAlignmentName(value)} تغییر کرد`);
|
||
break;
|
||
case "convert":
|
||
convertBlockType(value);
|
||
showToast(`بلوک به ${getBlockTypeName(value)} تبدیل شد`);
|
||
break;
|
||
case "duplicate":
|
||
duplicateBlock(blockData);
|
||
showToast("بلوک تکثیر شد");
|
||
break;
|
||
case "comment":
|
||
const comment = prompt("نظر خود را وارد کنید:");
|
||
if (comment) {
|
||
showToast("نظر اضافه شد");
|
||
}
|
||
break;
|
||
case "move-up":
|
||
showToast("بلوک به بالا منتقل شد");
|
||
break;
|
||
case "move-down":
|
||
showToast("بلوک به پایین منتقل شد");
|
||
break;
|
||
case "delete-block":
|
||
if (confirm("آیا مطمئنید که میخواهید این بلوک را حذف کنید؟")) {
|
||
deleteBlock();
|
||
showToast("بلوک حذف شد");
|
||
}
|
||
break;
|
||
case "ai-rewrite":
|
||
case "ai-expand":
|
||
case "ai-simplify":
|
||
const aiAction = action.replace("ai-", "");
|
||
handleAIAction(aiAction, getBlockContent());
|
||
break;
|
||
}
|
||
|
||
showBlockMenu.value = false;
|
||
};
|
||
|
||
// ==================== توابع کمکی ====================
|
||
|
||
const getAlignmentName = (value) => {
|
||
return alignmentNames[value] || value;
|
||
};
|
||
|
||
const getBlockTypeName = (type) => {
|
||
return blockTypeNames[type] || "بلوک";
|
||
};
|
||
|
||
const convertBlockType = (newType) => {
|
||
if (!editor.value) return;
|
||
|
||
switch (newType) {
|
||
case "paragraph":
|
||
editor.value.chain().focus().setParagraph().run();
|
||
break;
|
||
case "heading1":
|
||
editor.value.chain().focus().toggleHeading({ level: 1 }).run();
|
||
break;
|
||
case "heading2":
|
||
editor.value.chain().focus().toggleHeading({ level: 2 }).run();
|
||
break;
|
||
case "heading3":
|
||
editor.value.chain().focus().toggleHeading({ level: 3 }).run();
|
||
break;
|
||
case "todo":
|
||
editor.value.chain().focus().toggleTaskList().run();
|
||
break;
|
||
case "bullet":
|
||
editor.value.chain().focus().toggleBulletList().run();
|
||
break;
|
||
case "number":
|
||
editor.value.chain().focus().toggleOrderedList().run();
|
||
break;
|
||
case "code":
|
||
editor.value.chain().focus().toggleCodeBlock().run();
|
||
break;
|
||
case "quote":
|
||
editor.value.chain().focus().toggleBlockquote().run();
|
||
break;
|
||
}
|
||
};
|
||
|
||
function duplicateBlock(block) {
|
||
const ed = editor.value;
|
||
if (!ed) return;
|
||
|
||
const node = ed.state.doc.nodeAt(block.position);
|
||
if (!node) return;
|
||
|
||
ed.commands.insertContentAt(block.position + node.nodeSize, node.toJSON());
|
||
}
|
||
|
||
const deleteBlock = () => {
|
||
if (editor.value) {
|
||
const { from, to } = editor.value.state.selection;
|
||
editor.value.chain().focus().deleteRange({ from, to }).run();
|
||
}
|
||
};
|
||
|
||
const getBlockContent = () => {
|
||
if (!editor.value) return "";
|
||
const { from, to } = editor.value.state.selection;
|
||
return editor.value.state.doc.textBetween(from, to, " ");
|
||
};
|
||
|
||
// ==================== توابع AI ====================
|
||
|
||
// تابع اتصال به LLM
|
||
const handleAIAction = async (action, text, targetLang = null) => {
|
||
if (!text || !text.trim()) {
|
||
showToast("لطفاً متنی را انتخاب کنید");
|
||
return;
|
||
}
|
||
|
||
isAIProcessing.value = true;
|
||
|
||
try {
|
||
// آمادهسازی بدنه درخواست
|
||
const body = {
|
||
action,
|
||
block: {
|
||
type: "paragraph",
|
||
content: text,
|
||
attrs: {},
|
||
},
|
||
};
|
||
|
||
// فقط برای ترجمه زبان مقصد را اضافه کن
|
||
if (action === "translate" && targetLang) {
|
||
body.target_lang = targetLang;
|
||
}
|
||
|
||
// ارسال درخواست به بک
|
||
const response = await $fetch("http://localhost:8000/ai/block", {
|
||
method: "POST",
|
||
body,
|
||
});
|
||
|
||
console.log("🤖 پاسخ AI:", response);
|
||
|
||
if (response.content && editor.value) {
|
||
if (action === "title") {
|
||
const lines = response.content
|
||
.split("\n")
|
||
.map((l) => l.trim())
|
||
.filter(Boolean);
|
||
|
||
const title = lines[0];
|
||
const paragraph = lines.slice(1).join("\n");
|
||
|
||
editor.value
|
||
.chain()
|
||
.focus()
|
||
.deleteSelection()
|
||
.insertContent([
|
||
{
|
||
type: "paragraph",
|
||
content: [
|
||
{
|
||
type: "text",
|
||
text: title,
|
||
marks: [{ type: "bold" }],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
type: "paragraph",
|
||
content: [{ type: "text", text: paragraph }],
|
||
},
|
||
])
|
||
.run();
|
||
|
||
showToast("✅ تیتر بهصورت بولد اضافه شد");
|
||
} else if (action === "spellcheck") {
|
||
const highlightedContent = buildHighlightedContent(
|
||
text,
|
||
response.content,
|
||
);
|
||
|
||
editor.value
|
||
.chain()
|
||
.focus()
|
||
.deleteSelection()
|
||
.insertContent({
|
||
type: "paragraph",
|
||
content: highlightedContent,
|
||
})
|
||
.run();
|
||
|
||
showToast("✏️ اصلاحات املایی مشخص شد");
|
||
} else {
|
||
// بقیه اکشنها مثل قبل
|
||
editor.value
|
||
.chain()
|
||
.focus()
|
||
.deleteSelection()
|
||
.insertContent(response.content)
|
||
.run();
|
||
|
||
showToast("✅ متن آماده شد");
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("❌ خطای AI:", error);
|
||
showToast("خطا در اتصال به AI");
|
||
} finally {
|
||
isAIProcessing.value = false;
|
||
}
|
||
};
|
||
const buildHighlightedContent = (original, corrected) => {
|
||
const origWords = original.split(/\s+/);
|
||
const newWords = corrected.split(/\s+/);
|
||
|
||
return newWords.map((word, index) => {
|
||
const isChanged = origWords[index] !== word;
|
||
|
||
return {
|
||
type: "text",
|
||
text: word + " ",
|
||
marks: isChanged
|
||
? [{ type: "bold" }] // ← امن و بدون ریسک
|
||
: [],
|
||
};
|
||
});
|
||
};
|
||
|
||
const applyAIResult = (action, result) => {
|
||
if (!editor.value) return;
|
||
|
||
switch (action) {
|
||
case "summarize":
|
||
editor.value
|
||
.chain()
|
||
.focus()
|
||
.insertContent(
|
||
`
|
||
<div class="ai-result summary">
|
||
<h4>📝 خلاصه AI:</h4>
|
||
<p>${result}</p>
|
||
</div>
|
||
`,
|
||
)
|
||
.run();
|
||
break;
|
||
case "improve":
|
||
case "continue":
|
||
case "spellcheck":
|
||
case "simplify":
|
||
case "rewrite":
|
||
const { from, to } = editor.value.state.selection;
|
||
editor.value
|
||
.chain()
|
||
.focus()
|
||
.deleteRange({ from, to })
|
||
.insertContent(result)
|
||
.run();
|
||
break;
|
||
case "translate":
|
||
editor.value
|
||
.chain()
|
||
.focus()
|
||
.insertContent(
|
||
`
|
||
<div class="ai-result translation">
|
||
<h4>🌍 ترجمه AI:</h4>
|
||
<p>${result}</p>
|
||
</div>
|
||
`,
|
||
)
|
||
.run();
|
||
break;
|
||
case "explain":
|
||
case "simplify":
|
||
editor.value
|
||
.chain()
|
||
.focus()
|
||
.insertContent(
|
||
`
|
||
<div class="ai-result explanation">
|
||
<h4>💡 توضیح AI:</h4>
|
||
<p>${result}</p>
|
||
</div>
|
||
`,
|
||
)
|
||
.run();
|
||
break;
|
||
case "continue":
|
||
case "expand":
|
||
editor.value
|
||
.chain()
|
||
.focus()
|
||
.insertContent(
|
||
`
|
||
<div class="ai-result expansion">
|
||
<h4>↪️ ادامه AI:</h4>
|
||
<p>${result}</p>
|
||
</div>
|
||
`,
|
||
)
|
||
.run();
|
||
break;
|
||
}
|
||
};
|
||
|
||
// ==================== مدیریت بلوکهای قابل مشاهده ====================
|
||
|
||
const blocks = computed(() => {
|
||
if (!editor.value) return [];
|
||
|
||
const list = [];
|
||
|
||
editor.value.state.doc.forEach((node, pos) => {
|
||
list.push({
|
||
id: `${pos}-${node.type.name}`,
|
||
type: node.type.name,
|
||
position: pos,
|
||
content: node.textContent,
|
||
attrs: node.attrs || {},
|
||
});
|
||
});
|
||
|
||
return list;
|
||
});
|
||
|
||
const getBlockIconStyle = (block, index) => ({
|
||
top: `${index * 48 + 20}px`,
|
||
left: `-40px`,
|
||
});
|
||
|
||
const getBlockIcon = (type) => {
|
||
return blockIcons[type] || "📝";
|
||
};
|
||
|
||
// مدیریت لینک
|
||
const toggleLink = () => {
|
||
if (editor.isActive("link")) {
|
||
linkUrl.value = editor.getAttributes("link").href || "";
|
||
showLinkModal.value = true;
|
||
nextTick(() => {
|
||
if (linkInput.value) linkInput.value.focus();
|
||
});
|
||
} else {
|
||
showLinkModal.value = true;
|
||
linkUrl.value = "";
|
||
nextTick(() => {
|
||
if (linkInput.value) linkInput.value.focus();
|
||
});
|
||
}
|
||
};
|
||
|
||
const setLink = () => {
|
||
if (linkUrl.value) {
|
||
let url = linkUrl.value;
|
||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||
url = "https://" + url;
|
||
}
|
||
|
||
editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
|
||
}
|
||
closeLinkModal();
|
||
};
|
||
|
||
const unsetLink = () => {
|
||
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
||
closeLinkModal();
|
||
};
|
||
|
||
const closeLinkModal = () => {
|
||
showLinkModal.value = false;
|
||
linkUrl.value = "";
|
||
};
|
||
|
||
// نمایش toast
|
||
const showToast = (message) => {
|
||
const toast = document.createElement("div");
|
||
toast.className = "toast-message";
|
||
toast.textContent = message;
|
||
toast.style.cssText = `
|
||
position: fixed;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
background: #10b981;
|
||
color: white;
|
||
padding: 12px 24px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
z-index: 10000;
|
||
animation: toastIn 0.3s ease-out;
|
||
`;
|
||
|
||
document.body.appendChild(toast);
|
||
|
||
setTimeout(() => {
|
||
toast.style.animation = "toastOut 0.3s ease-out forwards";
|
||
setTimeout(() => {
|
||
document.body.removeChild(toast);
|
||
}, 300);
|
||
}, 3000);
|
||
};
|
||
|
||
// میانبر Ctrl+K برای لینک
|
||
const handleKeyDown = (event) => {
|
||
if ((event.ctrlKey || event.metaKey) && event.key === "k") {
|
||
event.preventDefault();
|
||
toggleLink();
|
||
}
|
||
};
|
||
|
||
onMounted(() => {
|
||
// اضافه کردن لینک Font Awesome
|
||
const link = document.createElement("link");
|
||
link.rel = "stylesheet";
|
||
link.href =
|
||
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css";
|
||
document.head.appendChild(link);
|
||
|
||
document.addEventListener("keydown", handleKeyDown);
|
||
pageStore.loadFromLocalStorage();
|
||
|
||
// اضافه کردن استایل toast و animation
|
||
const style = document.createElement("style");
|
||
style.textContent = `
|
||
@keyframes toastIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
@keyframes toastOut {
|
||
from {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
to {
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
}
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
document.removeEventListener("keydown", handleKeyDown);
|
||
|
||
if (editor.value) {
|
||
editor.value.destroy();
|
||
}
|
||
});
|
||
</script>
|
||
<style scoped>
|
||
.editor-shell {
|
||
padding: 1rem;
|
||
position: relative;
|
||
}
|
||
|
||
/* نوار ابزار یک خطی */
|
||
.menu-bar-sticky {
|
||
position: sticky;
|
||
top: 0;
|
||
background: white;
|
||
z-index: 1000;
|
||
padding: 0.5rem 0;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
margin-bottom: 1rem;
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.menu-bar {
|
||
width: 100%;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
white-space: nowrap;
|
||
padding: 0.5rem;
|
||
background: #f8fafc;
|
||
border-radius: 8px;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.toolbar-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
height: 40px;
|
||
padding: 0 0.5rem;
|
||
}
|
||
|
||
.toolbar-section {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
height: 100%;
|
||
}
|
||
|
||
.toolbar-divider {
|
||
width: 1px;
|
||
height: 24px;
|
||
background: #d1d5db;
|
||
margin: 0 4px;
|
||
}
|
||
|
||
.toolbar-btn {
|
||
width: 36px;
|
||
height: 36px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: white;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
color: #4b5563;
|
||
transition: all 0.2s ease;
|
||
padding: 0;
|
||
}
|
||
|
||
.toolbar-btn:hover {
|
||
background: #f3f4f6;
|
||
border-color: #9ca3af;
|
||
color: #111827;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.toolbar-btn.is-active {
|
||
background: #3b82f6;
|
||
color: white;
|
||
border-color: #3b82f6;
|
||
}
|
||
|
||
.toolbar-btn:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.toolbar-btn i {
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* انتخابها */
|
||
.heading-select,
|
||
.font-size-select {
|
||
height: 36px;
|
||
padding: 0 8px;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 6px;
|
||
background: white;
|
||
color: #4b5563;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
min-width: 100px;
|
||
}
|
||
|
||
.heading-select:hover,
|
||
.font-size-select:hover {
|
||
border-color: #9ca3af;
|
||
}
|
||
|
||
.heading-select:focus,
|
||
.font-size-select:focus {
|
||
outline: none;
|
||
border-color: #3b82f6;
|
||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||
}
|
||
|
||
/* انتخاب رنگ */
|
||
.color-picker-wrapper {
|
||
position: relative;
|
||
display: inline-block;
|
||
}
|
||
|
||
.color-picker {
|
||
position: absolute;
|
||
top: 100%;
|
||
left: 0;
|
||
opacity: 0;
|
||
width: 100%;
|
||
height: 36px;
|
||
cursor: pointer;
|
||
z-index: 10;
|
||
}
|
||
|
||
/* منوی بیشتر */
|
||
.more-menu-wrapper {
|
||
position: relative;
|
||
display: inline-block;
|
||
}
|
||
|
||
.more-dropdown {
|
||
position: absolute;
|
||
top: 100%;
|
||
left: 0;
|
||
background: white;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 8px;
|
||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||
padding: 8px;
|
||
min-width: 180px;
|
||
display: none;
|
||
z-index: 1000;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.more-menu-wrapper:hover .more-dropdown {
|
||
display: block;
|
||
}
|
||
|
||
.dropdown-item {
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 12px;
|
||
background: none;
|
||
border: none;
|
||
border-radius: 6px;
|
||
color: #4b5563;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
text-align: right;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.dropdown-item:hover {
|
||
background: #f3f4f6;
|
||
color: #111827;
|
||
}
|
||
|
||
.dropdown-divider {
|
||
height: 1px;
|
||
background: #e5e7eb;
|
||
margin: 8px 0;
|
||
}
|
||
|
||
/* وضعیت ذخیره */
|
||
.save-status {
|
||
text-align: center;
|
||
padding: 4px;
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
background: #f9fafb;
|
||
border-radius: 4px;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.saving {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.saved {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.check-icon {
|
||
color: #10b981;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.dot-pulse {
|
||
position: relative;
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background-color: #3b82f6;
|
||
animation: dot-pulse 1.5s infinite linear;
|
||
}
|
||
|
||
@keyframes dot-pulse {
|
||
0%,
|
||
60%,
|
||
100% {
|
||
opacity: 0.3;
|
||
transform: scale(0.8);
|
||
}
|
||
30% {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
|
||
.time {
|
||
font-size: 11px;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
/* محتوای ویرایشگر */
|
||
.editor-content-wrapper {
|
||
position: relative;
|
||
/* min-height: 60vh; */
|
||
margin-bottom: 80px;
|
||
}
|
||
|
||
/* آیکون بلوک */
|
||
.block-icon-wrapper {
|
||
position: absolute;
|
||
left: -40px;
|
||
cursor: pointer;
|
||
opacity: 0.3;
|
||
transition: opacity 0.2s;
|
||
z-index: 10;
|
||
}
|
||
|
||
.block-icon-wrapper:hover {
|
||
opacity: 1 !important;
|
||
}
|
||
|
||
.block-icon {
|
||
width: 28px;
|
||
height: 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: white;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
color: #6b7280;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.block-icon:hover {
|
||
background: #f3f4f6;
|
||
border-color: #9ca3af;
|
||
transform: scale(1.1);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
/* وضعیت پردازش AI */
|
||
.ai-processing-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 10001;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
|
||
.ai-processing {
|
||
background: white;
|
||
padding: 32px;
|
||
border-radius: 16px;
|
||
text-align: center;
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||
min-width: 300px;
|
||
}
|
||
|
||
.ai-spinner {
|
||
width: 48px;
|
||
height: 48px;
|
||
border: 3px solid #e0e7ff;
|
||
border-top-color: #8b5cf6;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin: 0 auto 20px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.ai-text {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
color: #4f46e5;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.ai-progress {
|
||
height: 4px;
|
||
background: #e0e7ff;
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.ai-progress::after {
|
||
content: "";
|
||
display: block;
|
||
height: 100%;
|
||
width: 60%;
|
||
background: linear-gradient(90deg, #8b5cf6, #ec4899);
|
||
animation: progress 2s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes progress {
|
||
0%,
|
||
100% {
|
||
transform: translateX(-100%);
|
||
}
|
||
50% {
|
||
transform: translateX(100%);
|
||
}
|
||
}
|
||
|
||
/* مودال لینک */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal {
|
||
background: white;
|
||
border-radius: 0.75rem;
|
||
width: 450px;
|
||
max-width: 90%;
|
||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||
animation: modal-appear 0.3s ease-out;
|
||
}
|
||
|
||
@keyframes modal-appear {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-20px) scale(0.95);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0) scale(1);
|
||
}
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 1.25rem 1.5rem;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.modal-header h3 {
|
||
margin: 0;
|
||
font-size: 1.125rem;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 1.5rem;
|
||
cursor: pointer;
|
||
color: #6b7280;
|
||
padding: 0.25rem;
|
||
border-radius: 0.25rem;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
background: #f3f4f6;
|
||
color: #374151;
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 1.5rem;
|
||
}
|
||
|
||
.link-input {
|
||
width: 100%;
|
||
padding: 0.75rem 1rem;
|
||
border: 2px solid #e5e7eb;
|
||
border-radius: 0.5rem;
|
||
font-size: 0.875rem;
|
||
direction: ltr;
|
||
transition: border-color 0.2s;
|
||
}
|
||
|
||
.link-input:focus {
|
||
outline: none;
|
||
border-color: #3b82f6;
|
||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||
}
|
||
|
||
.link-example {
|
||
font-size: 0.75rem;
|
||
color: #6b7280;
|
||
margin-top: 0.5rem;
|
||
text-align: right;
|
||
}
|
||
|
||
.modal-footer {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
padding: 1.25rem 1.5rem;
|
||
border-top: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.btn-primary {
|
||
flex: 1;
|
||
padding: 0.625rem 1rem;
|
||
background: #3b82f6;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 0.5rem;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: #2563eb;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.btn-secondary {
|
||
flex: 1;
|
||
padding: 0.625rem 1rem;
|
||
background: #f3f4f6;
|
||
color: #374151;
|
||
border: none;
|
||
border-radius: 0.5rem;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: #e5e7eb;
|
||
}
|
||
|
||
.btn-danger {
|
||
flex: 1;
|
||
padding: 0.625rem 1rem;
|
||
background: #ef4444;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 0.5rem;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background: #dc2626;
|
||
}
|
||
|
||
/* استایلهای ویرایشگر */
|
||
:deep(.notion-editor) {
|
||
height: calc(100dvh - 15em);
|
||
overflow-y: auto;
|
||
outline: none;
|
||
/* min-height: 60vh; */
|
||
padding: 0 1rem;
|
||
}
|
||
|
||
:deep(.notion-editor p) {
|
||
margin: 1rem 0;
|
||
line-height: 1.8;
|
||
color: #374151;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
:deep(.notion-editor h1) {
|
||
font-size: 2.25rem;
|
||
font-weight: 800;
|
||
margin: 2rem 0 1rem;
|
||
color: #111827;
|
||
/* border-bottom: 3px solid #f3f4f6; */
|
||
padding-bottom: 0.5rem;
|
||
}
|
||
|
||
:deep(.notion-editor h2) {
|
||
font-size: 1.875rem;
|
||
font-weight: 700;
|
||
margin: 1.75rem 0 0.75rem;
|
||
color: #1f2937;
|
||
}
|
||
|
||
:deep(.notion-editor h3) {
|
||
font-size: 1.5rem;
|
||
font-weight: 600;
|
||
margin: 1.5rem 0 0.5rem;
|
||
color: #374151;
|
||
}
|
||
|
||
:deep(.notion-editor ul),
|
||
:deep(.notion-editor ol) {
|
||
padding-right: 1.75rem;
|
||
margin: 1rem 0;
|
||
}
|
||
|
||
:deep(.notion-editor li) {
|
||
margin: 0.5rem 0;
|
||
padding-right: 0.5rem;
|
||
}
|
||
|
||
:deep(.notion-editor .task-list) {
|
||
padding-right: 0;
|
||
list-style: none;
|
||
}
|
||
|
||
:deep(.notion-editor .task-item) {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 0.75rem;
|
||
padding: 0.25rem 0;
|
||
}
|
||
|
||
:deep(.notion-editor .task-item input[type="checkbox"]) {
|
||
margin-top: 0.35rem;
|
||
cursor: pointer;
|
||
}
|
||
|
||
:deep(.notion-editor code) {
|
||
background: #f3f4f6;
|
||
padding: 0.2rem 0.4rem;
|
||
border-radius: 0.25rem;
|
||
font-family: "JetBrains Mono", "Courier New", monospace;
|
||
font-size: 0.875rem;
|
||
color: #dc2626;
|
||
}
|
||
|
||
:deep(.notion-editor pre) {
|
||
background: #1f2937;
|
||
color: #f9fafb;
|
||
padding: 1.25rem;
|
||
border-radius: 0.5rem;
|
||
overflow-x: auto;
|
||
direction: ltr;
|
||
text-align: left;
|
||
margin: 1.5rem 0;
|
||
font-family: "JetBrains Mono", "Courier New", monospace;
|
||
font-size: 0.875rem;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
:deep(.notion-editor blockquote) {
|
||
border-right: 4px solid #3b82f6;
|
||
padding-right: 1.5rem;
|
||
margin: 1.5rem 0;
|
||
color: #4b5563;
|
||
font-style: italic;
|
||
background: #f9fafb;
|
||
padding: 1.25rem;
|
||
border-radius: 0 0.75rem 0.75rem 0;
|
||
}
|
||
|
||
:deep(.notion-editor hr) {
|
||
border: none;
|
||
border-top: 2px solid #e5e7eb;
|
||
margin: 2rem 0;
|
||
}
|
||
|
||
:deep(.notion-editor a) {
|
||
color: #3b82f6;
|
||
text-decoration: none;
|
||
border-bottom: 1px solid transparent;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
:deep(.notion-editor a:hover) {
|
||
color: #2563eb;
|
||
border-bottom-color: #2563eb;
|
||
}
|
||
|
||
/* نتایج AI */
|
||
:deep(.ai-result) {
|
||
background: #f0f9ff;
|
||
border: 1px solid #0ea5e9;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
margin: 16px 0;
|
||
position: relative;
|
||
}
|
||
|
||
:deep(.ai-result h4) {
|
||
color: #0369a1;
|
||
margin-top: 0;
|
||
margin-bottom: 8px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
:deep(.ai-result p) {
|
||
margin: 0;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
/* ترازبندی متن */
|
||
:deep(.notion-editor [data-text-align="right"]) {
|
||
text-align: right;
|
||
}
|
||
|
||
:deep(.notion-editor [data-text-align="center"]) {
|
||
text-align: center;
|
||
}
|
||
|
||
:deep(.notion-editor [data-text-align="left"]) {
|
||
text-align: left;
|
||
}
|
||
|
||
:deep(.ProseMirror-focused) {
|
||
outline: none;
|
||
}
|
||
|
||
:deep(.is-editor-empty:first-child::before) {
|
||
content: attr(data-placeholder);
|
||
float: right;
|
||
color: #9ca3af;
|
||
pointer-events: none;
|
||
height: 0;
|
||
font-style: italic;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
/* ریسپانسیو */
|
||
@media (max-width: 768px) {
|
||
.editor-shell {
|
||
padding: 0.5rem;
|
||
}
|
||
|
||
.menu-bar-sticky {
|
||
position: relative;
|
||
top: 0;
|
||
margin-bottom: 1rem;
|
||
padding: 0.75rem 0;
|
||
}
|
||
|
||
.menu-bar {
|
||
gap: 0.25rem;
|
||
padding: 0.25rem;
|
||
}
|
||
|
||
.toolbar-container {
|
||
gap: 0.25rem;
|
||
padding: 0 0.25rem;
|
||
}
|
||
|
||
.toolbar-btn {
|
||
width: 32px;
|
||
height: 32px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.heading-select,
|
||
.font-size-select {
|
||
height: 32px;
|
||
font-size: 12px;
|
||
min-width: 80px;
|
||
}
|
||
|
||
.toolbar-btn i {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.more-dropdown {
|
||
left: auto;
|
||
right: 0;
|
||
}
|
||
|
||
.modal {
|
||
width: 95%;
|
||
}
|
||
|
||
.modal-footer {
|
||
flex-direction: column;
|
||
}
|
||
|
||
:deep(.notion-editor) {
|
||
padding: 0 0.5rem;
|
||
}
|
||
|
||
:deep(.notion-editor h1) {
|
||
font-size: 1.875rem;
|
||
}
|
||
|
||
:deep(.notion-editor h2) {
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
:deep(.notion-editor h3) {
|
||
font-size: 1.25rem;
|
||
}
|
||
|
||
.block-icon-wrapper {
|
||
left: -30px;
|
||
}
|
||
|
||
.test-controls {
|
||
left: 10px;
|
||
bottom: 10px;
|
||
}
|
||
|
||
.test-btn {
|
||
padding: 8px 12px;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.toolbar-container {
|
||
gap: 2px;
|
||
}
|
||
|
||
.toolbar-divider {
|
||
display: none;
|
||
}
|
||
|
||
.heading-select,
|
||
.font-size-select {
|
||
min-width: 60px;
|
||
font-size: 11px;
|
||
padding: 0 4px;
|
||
}
|
||
}
|
||
|
||
/* اسکرول بار سفارشی */
|
||
.menu-bar::-webkit-scrollbar {
|
||
height: 4px;
|
||
}
|
||
|
||
.menu-bar::-webkit-scrollbar-track {
|
||
background: #f1f1f1;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.menu-bar::-webkit-scrollbar-thumb {
|
||
background: #c1c1c1;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.menu-bar::-webkit-scrollbar-thumb:hover {
|
||
background: #a1a1a1;
|
||
}
|
||
|
||
.animate-pulse-glow {
|
||
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%,
|
||
100% {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
50% {
|
||
opacity: 0.8;
|
||
transform: scale(1.05);
|
||
}
|
||
}
|
||
</style>
|
||
<style lang="scss">
|
||
.ProseMirror [data-drag-handle] {
|
||
display: inline-block;
|
||
width: 18px;
|
||
height: 18px;
|
||
margin-left: -24px;
|
||
cursor: grab;
|
||
background: repeating-linear-gradient(
|
||
to bottom,
|
||
#999,
|
||
#999 2px,
|
||
transparent 2px,
|
||
transparent 4px
|
||
);
|
||
}
|
||
|
||
</style>
|