1+ <script setup lang="ts">
2+ import type { Editor } from ' @tiptap/vue-3'
3+ import { cn } from ' @/lib/utils'
4+ import { computed , onMounted , ref , watch , type HTMLAttributes } from ' vue'
5+ import { useTiptapContext } from ' .'
6+
7+ const props = defineProps <{
8+ editor? : Editor | null
9+ class? : HTMLAttributes [' class' ]
10+ placeholder? : string
11+ }>()
12+
13+ // Get editor from context if not provided directly
14+ const { editor : contextEditor } = useTiptapContext ()
15+ const editor = computed (() => props .editor ?? contextEditor .value )
16+
17+ // Editor is ready when it's available and initialized
18+ const isEditorReady = computed (() => {
19+ return editor .value && editor .value .isEditable
20+ })
21+
22+ // Reference to the editor element
23+ const editorElement = ref <HTMLElement | null >(null )
24+
25+ // Mount editor to DOM when editor is available or element changes
26+ watch ([editor , editorElement ], ([currentEditor , element ]) => {
27+ if (currentEditor && element && ! element .firstChild ) {
28+ // If editor is ready but not yet mounted to this element
29+ element .append (currentEditor .view .dom )
30+ }
31+ }, { immediate: true })
32+
33+ // Also mount on component mount in case both are already available
34+ onMounted (() => {
35+ if (isEditorReady .value && editorElement .value && ! editorElement .value .firstChild ) {
36+ editorElement .value .append (editor .value ! .view .dom )
37+ }
38+
39+ if (isEditorReady .value && editorElement .value && editorElement .value .firstChild ) {
40+ // Add appropriate ARIA attributes
41+ const editorDOM = editorElement .value .firstChild as HTMLElement
42+ editorDOM .setAttribute (' role' , ' textbox' )
43+ editorDOM .setAttribute (' aria-multiline' , ' true' )
44+ editorDOM .setAttribute (' aria-label' , ' Rich text editor' )
45+
46+ // Add content description for screen readers
47+ const srDescription = document .createElement (' span' )
48+ srDescription .className = ' sr-only'
49+ srDescription .textContent = ' Use keyboard shortcuts like Ctrl+B for bold, Ctrl+I for italic. Press Alt+F10 for the toolbar.'
50+ editorElement .value .appendChild (srDescription )
51+ }
52+ })
53+ </script >
54+
55+ <template >
56+ <div
57+ :class =" cn(
58+ 'tiptap-editor-wrapper relative',
59+ props.class
60+ )"
61+ data-slot =" tiptap-content"
62+ aria-label =" Rich text editor"
63+ >
64+ <div
65+ v-if =" isEditorReady"
66+ ref =" editorElement"
67+ class =" prose prose-sm sm:prose lg:prose-lg xl:prose-xl dark:prose-invert h-full w-full focus:outline-none max-w-none px-4 py-6"
68+ role =" region"
69+ />
70+
71+ <div
72+ v-else
73+ class =" tiptap-editor-placeholder h-full w-full flex items-center justify-center text-muted-foreground text-sm italic"
74+ aria-live =" polite"
75+ >
76+ {{ placeholder || 'Loading editor...' }}
77+ </div >
78+ </div >
79+ </template >
80+
81+ <style >
82+ /* Editor styles */
83+ .tiptap-editor-wrapper .ProseMirror {
84+ min-height : 100px ;
85+ height : 100% ;
86+ border : none ;
87+ outline : none ;
88+ }
89+
90+ .tiptap-editor-wrapper .ProseMirror p .is-editor-empty :first-child ::before {
91+ color : #adb5bd ;
92+ content : attr (data-placeholder );
93+ float : left ;
94+ height : 0 ;
95+ pointer-events : none ;
96+ }
97+
98+ /* Component styles */
99+ .tiptap-editor-wrapper .component-node {
100+ margin-top : 1rem ;
101+ margin-bottom : 1rem ;
102+ }
103+
104+ .tiptap-editor-wrapper .component-selected {
105+ outline : 2px solid #3b82f6 ;
106+ outline-offset : 2px ;
107+ }
108+
109+ /* Heading styles */
110+ .tiptap-editor-wrapper h1 {
111+ font-size : 2em ;
112+ margin-top : 0.67em ;
113+ margin-bottom : 0.67em ;
114+ }
115+
116+ .tiptap-editor-wrapper h2 {
117+ font-size : 1.5em ;
118+ margin-top : 0.83em ;
119+ margin-bottom : 0.83em ;
120+ }
121+
122+ .tiptap-editor-wrapper h3 {
123+ font-size : 1.17em ;
124+ margin-top : 1em ;
125+ margin-bottom : 1em ;
126+ }
127+
128+ /* Node selection styles */
129+ .tiptap-editor-wrapper .ProseMirror .is-node-selected {
130+ outline : 2px solid #3b82f6 ;
131+ outline-offset : 2px ;
132+ }
133+
134+ /* Placeholder for empty nodes */
135+ .tiptap-editor-wrapper .ProseMirror p .is-empty ::before {
136+ color : #adb5bd ;
137+ content : attr (data-placeholder );
138+ float : left ;
139+ height : 0 ;
140+ pointer-events : none ;
141+ }
142+
143+ /* Focus indicators for accessibility */
144+ .tiptap-editor-wrapper .ProseMirror :focus-visible {
145+ outline : 2px solid hsl (var (--primary ));
146+ outline-offset : 2px ;
147+ }
148+
149+ /* Better focus styles for interactive elements */
150+ .tiptap-editor-wrapper .ProseMirror * [data-interactive ]:focus-visible {
151+ outline : 2px solid hsl (var (--primary ));
152+ outline-offset : 2px ;
153+ }
154+
155+ /* High contrast mode support */
156+ @media (forced-colors: active) {
157+ .tiptap-editor-wrapper .ProseMirror :focus-visible {
158+ outline : 3px solid CanvasText;
159+ }
160+
161+ .tiptap-editor-wrapper .component-selected {
162+ outline : 3px solid Highlight ;
163+ }
164+ }
165+ </style >
0 commit comments