11import './MenuBar.scss'
22
3- import { useCallback , useState } from 'react'
3+ import { useCallback , useMemo , useRef , useState } from 'react'
44import { LuHeading1 , LuHeading2 , LuHeading3 , LuHeading4 } from 'react-icons/lu'
55import {
66 RiArrowGoBackLine ,
@@ -20,18 +20,25 @@ import {
2020 RiTableLine ,
2121 RiUnderline ,
2222} from 'react-icons/ri'
23- import { LoadingButton } from '@mui/lab'
2423import {
25- Dialog ,
26- DialogContent ,
27- DialogContentText ,
28- DialogTitle ,
29- } from '@mui/material'
24+ AlertDialog ,
25+ AlertDialogBody ,
26+ AlertDialogCloseButton ,
27+ AlertDialogContent ,
28+ AlertDialogFooter ,
29+ AlertDialogHeader ,
30+ AlertDialogOverlay ,
31+ Box ,
32+ useDisclosure ,
33+ } from '@chakra-ui/react'
34+ import { Button , Link } from '@opengovsg/design-system-react'
3035import { Editor } from '@tiptap/react'
3136import { parse } from 'node-html-parser'
3237
3338import Form from '@/components/Form'
39+ import { makeExternalLink } from '@/helpers/urls'
3440
41+ import { simpleSubstitute , type VariableInfoMap } from './utils'
3542import { BareEditor } from '.'
3643
3744enum MenuLabels {
@@ -221,76 +228,98 @@ const menuButtons = [
221228 } ,
222229]
223230
231+ const dialogPlaceholders : Partial < Record < MenuLabels , string > > = {
232+ [ MenuLabels . LinkSet ] : 'Enter a full URL with http prefix' ,
233+ [ MenuLabels . ImageAdd ] :
234+ 'Enter direct image link (e.g. https://file.go.gov.sg/clipplumber.png)' ,
235+ }
236+
224237interface MenuBarProps {
225238 editor : Editor | null
239+ variableMap : VariableInfoMap
226240}
227- export const MenuBar = ( { editor } : MenuBarProps ) => {
228- const [ showValueDialog , setShowValueDialog ] = useState ( false )
241+
242+ export const MenuBar = ( { editor, variableMap } : MenuBarProps ) => {
243+ const {
244+ isOpen : isDialogOpen ,
245+ onClose,
246+ onOpen : onDialogOpen ,
247+ } = useDisclosure ( )
248+ const cancelRef = useRef ( null )
229249 const [ dialogValue , setDialogValue ] = useState ( '' )
230250 const [ dialogLabel , setDialogLabel ] = useState < MenuLabels | null > ( null )
231- const onClickOverrides : { [ key : string ] : ( ) => void } = {
232- [ MenuLabels . LinkSet ] : useCallback ( ( ) => {
233- if ( ! editor ) {
234- return
235- }
236- const previousUrl = editor . getAttributes ( 'link' ) . href
237- setDialogLabel ( MenuLabels . LinkSet )
238- setDialogValue ( previousUrl )
239- setShowValueDialog ( true )
240- } , [ editor ] ) ,
241- [ MenuLabels . ImageAdd ] : useCallback ( ( ) => {
242- if ( ! editor ) {
243- return
244- }
245- setDialogLabel ( MenuLabels . ImageAdd )
246- setDialogValue ( '' )
247- setShowValueDialog ( true )
248- } , [ editor ] ) ,
249- }
250- const dialogOnSubmits : { [ key : string ] : ( ) => void } = {
251- [ MenuLabels . LinkSet ] : useCallback ( ( ) => {
252- if ( ! editor ) {
253- return
254- }
255- const url = dialogValue
256- // cancelled
257- if ( url === null ) {
258- return
259- }
260251
261- // empty
262- if ( url === '' ) {
263- editor . chain ( ) . focus ( ) . extendMarkRange ( 'link' ) . unsetLink ( ) . run ( )
252+ const onDialogClose = useCallback ( ( ) => {
253+ setDialogLabel ( null )
254+ setDialogValue ( '' )
255+ onClose ( )
256+ } , [ onClose ] )
264257
265- return
266- }
258+ const onClickOverrides : Partial < Record < MenuLabels , ( ) => void > > = useMemo (
259+ ( ) => ( {
260+ [ MenuLabels . LinkSet ] : ( ) => {
261+ if ( ! editor ) {
262+ return
263+ }
264+ const previousUrl = editor . getAttributes ( 'link' ) . href
265+ setDialogLabel ( MenuLabels . LinkSet )
266+ setDialogValue ( previousUrl )
267+ onDialogOpen ( )
268+ } ,
269+ [ MenuLabels . ImageAdd ] : ( ) => {
270+ if ( ! editor ) {
271+ return
272+ }
273+ setDialogLabel ( MenuLabels . ImageAdd )
274+ setDialogValue ( '' )
275+ onDialogOpen ( )
276+ } ,
277+ } ) ,
278+ [ editor , onDialogOpen ] ,
279+ )
280+
281+ const dialogOnSubmits : Partial < Record < MenuLabels , ( ) => void > > = useMemo (
282+ ( ) => ( {
283+ [ MenuLabels . LinkSet ] : ( ) => {
284+ if ( ! editor ) {
285+ return
286+ }
287+ const url = dialogValue
288+ // cancelled
289+ if ( url === null ) {
290+ return
291+ }
292+
293+ // empty
294+ if ( url === '' ) {
295+ editor . chain ( ) . focus ( ) . extendMarkRange ( 'link' ) . unsetLink ( ) . run ( )
296+ return
297+ }
298+
299+ // update link
300+ editor
301+ . chain ( )
302+ . focus ( )
303+ . extendMarkRange ( 'link' )
304+ . setLink ( { href : url } )
305+ . run ( )
306+ onDialogClose ( )
307+ } ,
308+ [ MenuLabels . ImageAdd ] : ( ) => {
309+ if ( ! editor ) {
310+ return
311+ }
312+ const url = dialogValue
313+ if ( url === null ) {
314+ return
315+ }
316+ editor . chain ( ) . focus ( ) . setImage ( { src : url } ) . run ( )
317+ onDialogClose ( )
318+ } ,
319+ } ) ,
320+ [ editor , dialogValue , onDialogClose ] ,
321+ )
267322
268- // update link
269- editor
270- . chain ( )
271- . focus ( )
272- . extendMarkRange ( 'link' )
273- . setLink ( { href : url } )
274- . run ( )
275- setShowValueDialog ( false )
276- } , [ editor , dialogValue , setShowValueDialog ] ) ,
277- [ MenuLabels . ImageAdd ] : useCallback ( ( ) => {
278- if ( ! editor ) {
279- return
280- }
281- const url = dialogValue
282- if ( url === null ) {
283- return
284- }
285- editor . chain ( ) . focus ( ) . setImage ( { src : url } ) . run ( )
286- setShowValueDialog ( false )
287- } , [ editor , dialogValue ] ) ,
288- }
289- const dialogPlaceholders : { [ key : string ] : string } = {
290- [ MenuLabels . LinkSet ] : 'Enter a full URL with http prefix' ,
291- [ MenuLabels . ImageAdd ] :
292- 'Enter direct image link (e.g. https://file.go.gov.sg/clipplumber.png)' ,
293- }
294323 if ( ! editor ) {
295324 return null
296325 }
@@ -316,8 +345,9 @@ export const MenuBar = ({ editor }: MenuBarProps) => {
316345 } }
317346 className = { `menu-item${ isActive ?.( editor ) ? ' is-active' : '' } ` }
318347 onClick = { ( ) => {
319- if ( onClickOverrides && onClickOverrides [ label ] ) {
320- onClickOverrides [ label ] ( )
348+ const clickOverride = onClickOverrides [ label ]
349+ if ( clickOverride ) {
350+ clickOverride ( )
321351 return
322352 }
323353 onClick ( editor )
@@ -328,25 +358,20 @@ export const MenuBar = ({ editor }: MenuBarProps) => {
328358 )
329359 } ) }
330360 </ div >
331- < Dialog
332- open = { showValueDialog }
333- className = "menubar-dialog"
334- onClose = { ( ) => setShowValueDialog ( false ) }
335- sx = { { alignItems : 'flex-start' , overflow : 'scroll' } }
336- PaperProps = { {
337- // so that the variable selector float can overlay
338- sx : { display : 'inline-table' } ,
339- } }
340- >
341- < DialogTitle > { dialogLabel } </ DialogTitle >
342- < DialogContent >
343- < DialogContentText
344- tabIndex = { - 1 }
345- component = "div"
346- className = "menubar-dialog-content"
347- >
348- { dialogLabel && (
349- < Form onSubmit = { ( ) => dialogOnSubmits [ dialogLabel ] ( ) } >
361+ { isDialogOpen && dialogLabel && (
362+ < AlertDialog
363+ leastDestructiveRef = { cancelRef }
364+ motionPreset = "none"
365+ returnFocusOnClose = { true }
366+ onClose = { onDialogClose }
367+ isOpen = { true }
368+ >
369+ < AlertDialogOverlay />
370+ < Form onSubmit = { ( ) => dialogOnSubmits [ dialogLabel ] ?.( ) } >
371+ < AlertDialogContent >
372+ < AlertDialogHeader > { dialogLabel } </ AlertDialogHeader >
373+ < AlertDialogCloseButton />
374+ < AlertDialogBody ref = { cancelRef } >
350375 < BareEditor
351376 // val is in HTML, need to parse back to plain text
352377 onChange = { ( val ) => setDialogValue ( parse ( val ) . textContent ) }
@@ -355,19 +380,33 @@ export const MenuBar = ({ editor }: MenuBarProps) => {
355380 variablesEnabled
356381 placeholder = { dialogPlaceholders [ dialogLabel ] }
357382 />
358- < LoadingButton
359- type = "submit"
360- variant = "contained"
361- color = "primary"
362- sx = { { boxShadow : 2 } }
363- >
364- Submit
365- </ LoadingButton >
366- </ Form >
367- ) }
368- </ DialogContentText >
369- </ DialogContent >
370- </ Dialog >
383+ < Box p = { 2 } >
384+ < Link
385+ isExternal
386+ referrerPolicy = "no-referrer"
387+ isDisabled = { ! dialogValue }
388+ target = "_blank"
389+ href = {
390+ dialogValue
391+ ? makeExternalLink (
392+ simpleSubstitute ( dialogValue , variableMap ) ,
393+ )
394+ : undefined
395+ }
396+ >
397+ Open link
398+ </ Link >
399+ </ Box >
400+ </ AlertDialogBody >
401+ < AlertDialogFooter >
402+ < Button colorScheme = "primary" type = "submit" >
403+ Done
404+ </ Button >
405+ </ AlertDialogFooter >
406+ </ AlertDialogContent >
407+ </ Form >
408+ </ AlertDialog >
409+ ) }
371410 </ >
372411 )
373412}
0 commit comments