11import { createPortal } from 'react-dom'
2- import { useState , useEffect } from 'react'
2+ import { useState , useEffect , useRef , useCallback } from 'react'
33import { useDispatch , useSelector } from 'react-redux'
44import PropTypes from 'prop-types'
55import PerfectScrollbar from 'react-perfect-scrollbar'
66
77// MUI
8- import { Button , Dialog , DialogActions , DialogContent , Typography , Box } from '@mui/material'
8+ import { Button , Dialog , DialogActions , DialogContent , Typography , Box , ToggleButton , ToggleButtonGroup } from '@mui/material'
99import { styled } from '@mui/material/styles'
10+ import { IconCode , IconPencil } from '@tabler/icons-react'
1011
1112// Project Import
1213import { StyledButton } from '@/ui-component/button/StyledButton'
@@ -16,6 +17,7 @@ import { useEditor, EditorContent } from '@tiptap/react'
1617import Placeholder from '@tiptap/extension-placeholder'
1718import { mergeAttributes } from '@tiptap/core'
1819import StarterKit from '@tiptap/starter-kit'
20+ import { Markdown } from '@tiptap/markdown'
1921import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
2022import { common , createLowlight } from 'lowlight'
2123import { suggestionOptions } from '@/ui-component/input/suggestionOption'
@@ -24,10 +26,16 @@ import { CustomMention } from '@/utils/customMention'
2426
2527const lowlight = createLowlight ( common )
2628
29+ // Detect if content is legacy HTML (from old getHTML() storage) vs markdown
30+ const isHtmlContent = ( content ) => {
31+ if ( ! content || typeof content !== 'string' ) return false
32+ return / < (?: p | d i v | s p a n | h [ 1 - 6 ] | u l | o l | l i | b r | c o d e | p r e | b l o c k q u o t e | t a b l e | s t r o n g | e m ) \b / i. test ( content )
33+ }
34+
2735// Store
2836import { HIDE_CANVAS_DIALOG , SHOW_CANVAS_DIALOG } from '@/store/actions'
2937
30- // Add styled component for editor wrapper
38+ // Styled editor content for preview mode
3139const StyledEditorContent = styled ( EditorContent ) ( ( { theme, rows, disabled, isDarkMode } ) => ( {
3240 '& .ProseMirror' : {
3341 padding : '0px 14px' ,
@@ -36,7 +44,8 @@ const StyledEditorContent = styled(EditorContent)(({ theme, rows, disabled, isDa
3644 overflowX : rows ? 'auto' : 'hidden' ,
3745 lineHeight : rows ? '1.4375em' : '0.875em' ,
3846 fontWeight : 500 ,
39- color : disabled ? theme . palette . action . disabled : theme . palette . grey [ 900 ] ,
47+ color : theme . palette . grey [ 900 ] ,
48+ opacity : disabled ? 0.7 : 1 ,
4049 border : `1px solid ${ theme . palette . grey [ 900 ] + 25 } ` ,
4150 borderRadius : '10px' ,
4251 backgroundColor : theme . palette . textBackground . main ,
@@ -54,7 +63,7 @@ const StyledEditorContent = styled(EditorContent)(({ theme, rows, disabled, isDa
5463 '& p.is-editor-empty:first-of-type::before' : {
5564 content : 'attr(data-placeholder)' ,
5665 float : 'left' ,
57- color : disabled ? theme . palette . action . disabled : theme . palette . text . primary ,
66+ color : theme . palette . text . primary ,
5867 opacity : disabled ? 0.6 : 0.4 ,
5968 pointerEvents : 'none' ,
6069 height : 0
@@ -73,11 +82,45 @@ const StyledEditorContent = styled(EditorContent)(({ theme, rows, disabled, isDa
7382 }
7483} ) )
7584
85+ // Styled textarea for raw mode
86+ const StyledTextarea = styled ( 'textarea' ) ( ( { theme, disabled } ) => ( {
87+ width : '100%' ,
88+ padding : '8px 14px' ,
89+ height : `${ 15 * 1.4375 } rem` ,
90+ overflowY : 'auto' ,
91+ overflowX : 'auto' ,
92+ lineHeight : '1.4375em' ,
93+ fontWeight : 500 ,
94+ fontFamily : 'inherit' ,
95+ fontSize : 'inherit' ,
96+ color : theme . palette . grey [ 900 ] ,
97+ opacity : disabled ? 0.7 : 1 ,
98+ border : `1px solid ${ theme . palette . grey [ 900 ] + 25 } ` ,
99+ borderRadius : '10px' ,
100+ backgroundColor : theme . palette . textBackground . main ,
101+ boxSizing : 'border-box' ,
102+ resize : 'none' ,
103+ outline : 'none' ,
104+ whiteSpace : 'pre-wrap' ,
105+ '&:hover' : {
106+ borderColor : disabled ? `${ theme . palette . grey [ 900 ] + 25 } ` : theme . palette . text . primary ,
107+ cursor : disabled ? 'default' : 'text'
108+ } ,
109+ '&:focus' : {
110+ borderColor : disabled ? `${ theme . palette . grey [ 900 ] + 25 } ` : theme . palette . primary . main
111+ } ,
112+ '&::placeholder' : {
113+ color : theme . palette . text . primary ,
114+ opacity : disabled ? 0.6 : 0.4
115+ }
116+ } ) )
117+
76118// define your extension array
77119const extensions = ( availableNodesForVariable , availableState , acceptNodeOutputAsVariable , nodes , nodeData , isNodeInsideInteration ) => [
78120 StarterKit . configure ( {
79121 codeBlock : false
80122 } ) ,
123+ Markdown ,
81124 CustomMention . configure ( {
82125 HTMLAttributes : {
83126 class : 'variable'
@@ -113,12 +156,14 @@ const ExpandRichInputDialog = ({ show, dialogProps, onCancel, onInputHintDialogC
113156 const customization = useSelector ( ( state ) => state . customization )
114157 const isDarkMode = customization . isDarkMode
115158
159+ const [ viewMode , setViewMode ] = useState ( 'preview' )
116160 const [ inputValue , setInputValue ] = useState ( '' )
117161 const [ inputParam , setInputParam ] = useState ( null )
118162 const [ availableNodesForVariable , setAvailableNodesForVariable ] = useState ( [ ] )
119163 const [ availableState , setAvailableState ] = useState ( [ ] )
120164 const [ nodeData , setNodeData ] = useState ( { } )
121165 const [ isNodeInsideInteration , setIsNodeInsideInteration ] = useState ( false )
166+ const isSwitchingRef = useRef ( false )
122167
123168 useEffect ( ( ) => {
124169 if ( dialogProps . value ) {
@@ -131,6 +176,7 @@ const ExpandRichInputDialog = ({ show, dialogProps, onCancel, onInputHintDialogC
131176 return ( ) => {
132177 setInputValue ( '' )
133178 setInputParam ( null )
179+ setViewMode ( 'preview' )
134180 }
135181 } , [ dialogProps ] )
136182
@@ -171,33 +217,104 @@ const ExpandRichInputDialog = ({ show, dialogProps, onCancel, onInputHintDialogC
171217 ) ,
172218 Placeholder . configure ( { placeholder : inputParam ?. placeholder } )
173219 ] ,
174- content : inputValue ,
220+ content : '' ,
175221 onUpdate : ( { editor } ) => {
176- setInputValue ( editor . getHTML ( ) )
222+ if ( ! isSwitchingRef . current ) {
223+ try {
224+ setInputValue ( editor . getMarkdown ( ) )
225+ } catch {
226+ setInputValue ( editor . getHTML ( ) )
227+ }
228+ }
177229 } ,
178230 editable : ! dialogProps . disabled
179231 } ,
180232 [ availableNodesForVariable ]
181233 )
182234
183- // Focus the editor when dialog opens
235+ // Load content into the editor once it's ready
236+ useEffect ( ( ) => {
237+ if ( editor && inputValue ) {
238+ isSwitchingRef . current = true
239+ if ( isHtmlContent ( inputValue ) ) {
240+ editor . commands . setContent ( inputValue )
241+ try {
242+ setInputValue ( editor . getMarkdown ( ) )
243+ } catch {
244+ // keep original value if conversion fails
245+ }
246+ } else {
247+ editor . commands . setContent ( inputValue , { contentType : 'markdown' } )
248+ }
249+ isSwitchingRef . current = false
250+ }
251+ } , [ editor ] ) // eslint-disable-line react-hooks/exhaustive-deps
252+
253+ // Focus the editor when dialog opens in preview mode
184254 useEffect ( ( ) => {
185- if ( show && editor ) {
255+ if ( show && editor && viewMode === 'preview' ) {
186256 setTimeout ( ( ) => {
187257 editor . commands . focus ( )
188258 } , 100 )
189259 }
190- } , [ show , editor ] )
260+ } , [ show , editor , viewMode ] )
261+
262+ const handleViewModeChange = useCallback (
263+ ( event , newMode ) => {
264+ if ( ! newMode || newMode === viewMode ) return
265+
266+ if ( newMode === 'preview' && editor ) {
267+ isSwitchingRef . current = true
268+ const contentType = isHtmlContent ( inputValue ) ? 'html' : 'markdown'
269+ editor . commands . setContent ( inputValue , { contentType } )
270+ isSwitchingRef . current = false
271+ setTimeout ( ( ) => editor . commands . focus ( ) , 50 )
272+ } else if ( newMode === 'raw' && editor ) {
273+ try {
274+ setInputValue ( editor . getMarkdown ( ) )
275+ } catch {
276+ setInputValue ( editor . getHTML ( ) )
277+ }
278+ }
279+
280+ setViewMode ( newMode )
281+ } ,
282+ [ viewMode , editor , inputValue ]
283+ )
191284
192285 const component = show ? (
193286 < Dialog open = { show } fullWidth maxWidth = 'md' aria-labelledby = 'alert-dialog-title' aria-describedby = 'alert-dialog-description' >
194287 < DialogContent >
195288 < div style = { { display : 'flex' , flexDirection : 'row' } } >
196289 { inputParam && (
197290 < div style = { { flex : 70 , width : '100%' } } >
198- < div style = { { marginBottom : '10px' , display : 'flex' , flexDirection : 'row' } } >
291+ < div style = { { marginBottom : '10px' , display : 'flex' , flexDirection : 'row' , alignItems : 'center' } } >
199292 < Typography variant = 'h4' > { inputParam . label } </ Typography >
200293 < div style = { { flex : 1 } } />
294+ < ToggleButtonGroup
295+ value = { viewMode }
296+ exclusive
297+ onChange = { handleViewModeChange }
298+ size = 'small'
299+ sx = { {
300+ mr : inputParam . hint ? 1 : 0 ,
301+ '& .MuiToggleButton-root' : {
302+ color : isDarkMode ? 'rgba(255,255,255,0.5)' : undefined ,
303+ '&.Mui-selected' : {
304+ color : isDarkMode ? '#fff' : undefined
305+ }
306+ }
307+ } }
308+ >
309+ < ToggleButton value = 'preview' sx = { { px : 1.5 , py : 0.5 , textTransform : 'none' } } >
310+ < IconPencil size = { 16 } style = { { marginRight : 4 } } />
311+ Edit
312+ </ ToggleButton >
313+ < ToggleButton value = 'raw' sx = { { px : 1.5 , py : 0.5 , textTransform : 'none' } } >
314+ < IconCode size = { 16 } style = { { marginRight : 4 } } />
315+ Source
316+ </ ToggleButton >
317+ </ ToggleButtonGroup >
201318 { inputParam . hint && (
202319 < Button
203320 sx = { { p : 0 , px : 2 } }
@@ -219,14 +336,25 @@ const ExpandRichInputDialog = ({ show, dialogProps, onCancel, onInputHintDialogC
219336 overflowX : 'hidden'
220337 } }
221338 >
222- < Box sx = { { mt : 1 , border : '' } } >
223- < StyledEditorContent
224- editor = { editor }
225- rows = { 15 }
226- disabled = { dialogProps . disabled }
227- isDarkMode = { isDarkMode }
228- />
229- </ Box >
339+ { viewMode === 'raw' ? (
340+ < Box sx = { { mt : 1 } } >
341+ < StyledTextarea
342+ value = { inputValue }
343+ onChange = { ( e ) => setInputValue ( e . target . value ) }
344+ placeholder = { inputParam ?. placeholder }
345+ disabled = { dialogProps . disabled }
346+ />
347+ </ Box >
348+ ) : (
349+ < Box sx = { { mt : 1 } } >
350+ < StyledEditorContent
351+ editor = { editor }
352+ rows = { 15 }
353+ disabled = { dialogProps . disabled }
354+ isDarkMode = { isDarkMode }
355+ />
356+ </ Box >
357+ ) }
230358 </ PerfectScrollbar >
231359 </ div >
232360 ) }
0 commit comments