@ -9,6 +9,7 @@
|
||||
"lint": "dotenv -e ./.env -e ../../.env -- next lint",
|
||||
"test": "dotenv -e ./.env -e ../../.env -- pnpm playwright test",
|
||||
"test:show-report": "pnpm playwright show-report src/test/reporters",
|
||||
"test:ui": "dotenv -e ./.env -e ../../.env -- pnpm playwright test --ui",
|
||||
"format:check": "prettier --check ./src"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -42,12 +43,12 @@
|
||||
"@typebot.io/env": "workspace:*",
|
||||
"@typebot.io/js": "workspace:*",
|
||||
"@typebot.io/nextjs": "workspace:*",
|
||||
"@udecode/plate-basic-marks": "21.1.5",
|
||||
"@udecode/plate-common": "21.1.5",
|
||||
"@udecode/plate-core": "21.1.5",
|
||||
"@udecode/plate-link": "21.2.0",
|
||||
"@udecode/plate-ui-link": "21.2.0",
|
||||
"@udecode/plate-ui-toolbar": "21.1.5",
|
||||
"@udecode/cn": "29.0.1",
|
||||
"@udecode/plate-basic-marks": "30.5.3",
|
||||
"@udecode/plate-common": "30.4.5",
|
||||
"@udecode/plate-core": "30.4.5",
|
||||
"@udecode/plate-floating": "30.5.3",
|
||||
"@udecode/plate-link": "30.5.3",
|
||||
"@uiw/codemirror-extensions-langs": "4.21.7",
|
||||
"@uiw/codemirror-theme-github": "4.21.7",
|
||||
"@uiw/codemirror-theme-tokyo-night": "4.21.7",
|
||||
@ -83,9 +84,6 @@
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"slate": "0.94.1",
|
||||
"slate-history": "0.93.0",
|
||||
"slate-react": "0.94.2",
|
||||
"sonner": "1.3.1",
|
||||
"stripe": "12.13.0",
|
||||
"svg-round-corners": "0.4.1",
|
||||
@ -97,7 +95,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chakra-ui/styled-system": "2.9.1",
|
||||
"@playwright/test": "1.36.0",
|
||||
"@playwright/test": "1.41.2",
|
||||
"@typebot.io/forge": "workspace:*",
|
||||
"@typebot.io/forge-repository": "workspace:*",
|
||||
"@typebot.io/forge-schemas": "workspace:*",
|
||||
|
@ -651,3 +651,14 @@ export const LightBulbIcon = (props: IconProps) => (
|
||||
<path d="M10 22h4" />
|
||||
</Icon>
|
||||
)
|
||||
|
||||
export const UnlinkIcon = (props: IconProps) => (
|
||||
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||
<path d="m18.84 12.25 1.72-1.71h-.02a5.004 5.004 0 0 0-.12-7.07 5.006 5.006 0 0 0-6.95 0l-1.72 1.71" />
|
||||
<path d="m5.17 11.75-1.71 1.71a5.004 5.004 0 0 0 .12 7.07 5.006 5.006 0 0 0 6.95 0l1.71-1.71" />
|
||||
<line x1="8" x2="8" y1="2" y2="5" />
|
||||
<line x1="2" x2="5" y1="8" y2="8" />
|
||||
<line x1="16" x2="16" y1="19" y2="22" />
|
||||
<line x1="19" x2="22" y1="16" y2="16" />
|
||||
</Icon>
|
||||
)
|
||||
|
@ -1,175 +1,10 @@
|
||||
import {
|
||||
Flex,
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
PopoverContent,
|
||||
Portal,
|
||||
Stack,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Plate, PlateProvider, usePlateEditorRef } from '@udecode/plate-core'
|
||||
import { editorStyle, platePlugins } from '@/lib/plate'
|
||||
import { BaseEditor, BaseSelection, Transforms } from 'slate'
|
||||
import { Variable } from '@typebot.io/schemas'
|
||||
import { ReactEditor } from 'slate-react'
|
||||
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
|
||||
import { colors } from '@/lib/theme'
|
||||
import { useOutsideClick } from '@/hooks/useOutsideClick'
|
||||
import { selectEditor, TElement } from '@udecode/plate-common'
|
||||
import { TextEditorToolBar } from './TextEditorToolBar'
|
||||
import { useTranslate } from '@tolgee/react'
|
||||
import React, { useState } from 'react'
|
||||
import { Plate } from '@udecode/plate-core'
|
||||
import { platePlugins } from '@/lib/plate'
|
||||
import { TElement } from '@udecode/plate-common'
|
||||
import { TextEditorEditorContent } from './TextEditorEditorContent'
|
||||
|
||||
type TextBubbleEditorContentProps = {
|
||||
id: string
|
||||
textEditorValue: TElement[]
|
||||
onClose: (newContent: TElement[]) => void
|
||||
}
|
||||
|
||||
const TextBubbleEditorContent = ({
|
||||
id,
|
||||
textEditorValue,
|
||||
onClose,
|
||||
}: TextBubbleEditorContentProps) => {
|
||||
const { t } = useTranslate()
|
||||
const editor = usePlateEditorRef()
|
||||
const varDropdownRef = useRef<HTMLDivElement | null>(null)
|
||||
const rememberedSelection = useRef<BaseSelection | null>(null)
|
||||
const [isVariableDropdownOpen, setIsVariableDropdownOpen] = useState(false)
|
||||
const [isFirstFocus, setIsFirstFocus] = useState(true)
|
||||
|
||||
const textEditorRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const closeEditor = () => onClose(textEditorValue)
|
||||
|
||||
useOutsideClick({
|
||||
ref: textEditorRef,
|
||||
handler: closeEditor,
|
||||
})
|
||||
|
||||
const computeTargetCoord = useCallback(() => {
|
||||
if (rememberedSelection.current) return { top: 0, left: 0 }
|
||||
const selection = window.getSelection()
|
||||
const relativeParent = textEditorRef.current
|
||||
if (!selection || !relativeParent) return { top: 0, left: 0 }
|
||||
const range = selection.getRangeAt(0)
|
||||
const selectionBoundingRect = range.getBoundingClientRect()
|
||||
const relativeRect = relativeParent.getBoundingClientRect()
|
||||
return {
|
||||
top: selectionBoundingRect.bottom - relativeRect.top,
|
||||
left: selectionBoundingRect.left - relativeRect.left,
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVariableDropdownOpen) return
|
||||
const el = varDropdownRef.current
|
||||
if (!el) return
|
||||
const { top, left } = computeTargetCoord()
|
||||
if (top === 0 && left === 0) return
|
||||
el.style.top = `${top}px`
|
||||
el.style.left = `${left}px`
|
||||
}, [computeTargetCoord, isVariableDropdownOpen])
|
||||
|
||||
const handleVariableSelected = (variable?: Variable) => {
|
||||
setIsVariableDropdownOpen(false)
|
||||
if (!rememberedSelection.current || !variable) return
|
||||
ReactEditor.focus(editor as unknown as ReactEditor)
|
||||
Transforms.select(
|
||||
editor as unknown as BaseEditor,
|
||||
rememberedSelection.current
|
||||
)
|
||||
Transforms.insertText(
|
||||
editor as unknown as BaseEditor,
|
||||
'{{' + variable.name + '}}'
|
||||
)
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.shiftKey) return
|
||||
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) closeEditor()
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
flex="1"
|
||||
ref={textEditorRef}
|
||||
borderWidth="2px"
|
||||
borderColor="blue.400"
|
||||
rounded="md"
|
||||
pos="relative"
|
||||
spacing={0}
|
||||
cursor="text"
|
||||
className="prevent-group-drag"
|
||||
onContextMenuCapture={(e) => e.stopPropagation()}
|
||||
sx={{
|
||||
'.slate-ToolbarButton-active': {
|
||||
color: useColorModeValue('blue.500', 'blue.300') + ' !important',
|
||||
},
|
||||
'[class^="PlateFloatingLink___Styled"]': {
|
||||
'--tw-bg-opacity': useColorModeValue('1', '.1') + '!important',
|
||||
backgroundColor: useColorModeValue('white', 'gray.800'),
|
||||
borderRadius: 'md',
|
||||
transitionProperty: 'background-color',
|
||||
transitionDuration: 'normal',
|
||||
},
|
||||
'[class^="FloatingVerticalDivider___"]': {
|
||||
'--tw-bg-opacity': useColorModeValue('1', '.4') + '!important',
|
||||
},
|
||||
'.slate-a': {
|
||||
color: useColorModeValue('blue.500', 'blue.300'),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TextEditorToolBar
|
||||
onVariablesButtonClick={() => setIsVariableDropdownOpen(true)}
|
||||
/>
|
||||
<Plate
|
||||
id={id}
|
||||
editableProps={{
|
||||
style: editorStyle(useColorModeValue('white', colors.gray[850])),
|
||||
autoFocus: true,
|
||||
onFocus: () => {
|
||||
rememberedSelection.current = null
|
||||
if (!isFirstFocus) return
|
||||
if (editor.children.length === 0) return
|
||||
selectEditor(editor, {
|
||||
edge: 'end',
|
||||
})
|
||||
setIsFirstFocus(false)
|
||||
},
|
||||
'aria-label': `${t('editor.blocks.bubbles.textEditor.plate.label')}`,
|
||||
onBlur: () => {
|
||||
rememberedSelection.current = editor?.selection
|
||||
},
|
||||
onKeyDown: handleKeyDown,
|
||||
onClick: () => {
|
||||
setIsVariableDropdownOpen(false)
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Popover isOpen={isVariableDropdownOpen} isLazy>
|
||||
<PopoverAnchor>
|
||||
<Flex pos="absolute" ref={varDropdownRef} />
|
||||
</PopoverAnchor>
|
||||
<Portal>
|
||||
<PopoverContent>
|
||||
<VariableSearchInput
|
||||
initialVariableId={undefined}
|
||||
onSelectVariable={handleVariableSelected}
|
||||
placeholder={t(
|
||||
'editor.blocks.bubbles.textEditor.searchVariable.placeholder'
|
||||
)}
|
||||
autoFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
</Popover>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
type TextBubbleEditorProps = {
|
||||
id: string
|
||||
initialValue: TElement[]
|
||||
onClose: (newContent: TElement[]) => void
|
||||
@ -179,11 +14,14 @@ export const TextBubbleEditor = ({
|
||||
id,
|
||||
initialValue,
|
||||
onClose,
|
||||
}: TextBubbleEditorProps) => {
|
||||
const [textEditorValue, setTextEditorValue] = useState(initialValue)
|
||||
}: TextBubbleEditorContentProps) => {
|
||||
const [textEditorValue, setTextEditorValue] =
|
||||
useState<TElement[]>(initialValue)
|
||||
|
||||
const closeEditor = () => onClose(textEditorValue)
|
||||
|
||||
return (
|
||||
<PlateProvider
|
||||
<Plate
|
||||
id={id}
|
||||
plugins={platePlugins}
|
||||
initialValue={
|
||||
@ -193,11 +31,7 @@ export const TextBubbleEditor = ({
|
||||
}
|
||||
onChange={setTextEditorValue}
|
||||
>
|
||||
<TextBubbleEditorContent
|
||||
id={id}
|
||||
textEditorValue={textEditorValue}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</PlateProvider>
|
||||
<TextEditorEditorContent closeEditor={closeEditor} />
|
||||
</Plate>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,152 @@
|
||||
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
|
||||
import { editorStyle } from '@/lib/plate'
|
||||
import { colors } from '@/lib/theme'
|
||||
import {
|
||||
useColorModeValue,
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
Flex,
|
||||
Portal,
|
||||
PopoverContent,
|
||||
Stack,
|
||||
} from '@chakra-ui/react'
|
||||
import { Variable } from '@typebot.io/schemas'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { TextEditorToolBar } from './TextEditorToolBar'
|
||||
import { useTranslate } from '@tolgee/react'
|
||||
import { PlateContent, useEditorRef } from '@udecode/plate-core'
|
||||
import { focusEditor, insertText, selectEditor } from '@udecode/plate-common'
|
||||
import { useOutsideClick } from '@/hooks/useOutsideClick'
|
||||
|
||||
type Props = {
|
||||
closeEditor: () => void
|
||||
}
|
||||
export const TextEditorEditorContent = ({ closeEditor }: Props) => {
|
||||
const { t } = useTranslate()
|
||||
const editor = useEditorRef()
|
||||
const [isVariableDropdownOpen, setIsVariableDropdownOpen] = useState(false)
|
||||
const [isFirstFocus, setIsFirstFocus] = useState(true)
|
||||
|
||||
const varDropdownRef = useRef<HTMLDivElement | null>(null)
|
||||
const rememberedSelection = useRef<typeof editor.selection | null>(null)
|
||||
const textEditorRef = useRef<HTMLDivElement>(null)
|
||||
const plateContentRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleVariableSelected = (variable?: Variable) => {
|
||||
setIsVariableDropdownOpen(false)
|
||||
if (!variable) return
|
||||
focusEditor(editor)
|
||||
insertText(editor, '{{' + variable.name + '}}')
|
||||
}
|
||||
|
||||
useOutsideClick({
|
||||
ref: textEditorRef,
|
||||
handler: closeEditor,
|
||||
})
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.shiftKey) return
|
||||
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) closeEditor()
|
||||
}
|
||||
|
||||
const computeTargetCoord = useCallback(() => {
|
||||
if (rememberedSelection.current) return { top: 0, left: 0 }
|
||||
const selection = window.getSelection()
|
||||
const relativeParent = textEditorRef.current
|
||||
if (!selection || !relativeParent) return { top: 0, left: 0 }
|
||||
const range = selection.getRangeAt(0)
|
||||
const selectionBoundingRect = range.getBoundingClientRect()
|
||||
const relativeRect = relativeParent.getBoundingClientRect()
|
||||
return {
|
||||
top: selectionBoundingRect.bottom - relativeRect.top,
|
||||
left: selectionBoundingRect.left - relativeRect.left,
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVariableDropdownOpen) return
|
||||
const el = varDropdownRef.current
|
||||
if (!el) return
|
||||
const { top, left } = computeTargetCoord()
|
||||
if (top === 0 && left === 0) return
|
||||
el.style.top = `${top}px`
|
||||
el.style.left = `${left}px`
|
||||
}, [computeTargetCoord, isVariableDropdownOpen])
|
||||
|
||||
return (
|
||||
<Stack
|
||||
flex="1"
|
||||
ref={textEditorRef}
|
||||
borderWidth="2px"
|
||||
borderColor="blue.400"
|
||||
rounded="md"
|
||||
pos="relative"
|
||||
spacing={0}
|
||||
cursor="text"
|
||||
className="prevent-group-drag"
|
||||
onContextMenuCapture={(e) => e.stopPropagation()}
|
||||
sx={{
|
||||
'.slate-ToolbarButton-active': {
|
||||
color: useColorModeValue('blue.500', 'blue.300') + ' !important',
|
||||
},
|
||||
'[class^="PlateFloatingLink___Styled"]': {
|
||||
'--tw-bg-opacity': useColorModeValue('1', '.1') + '!important',
|
||||
backgroundColor: useColorModeValue('white', 'gray.800'),
|
||||
borderRadius: 'md',
|
||||
transitionProperty: 'background-color',
|
||||
transitionDuration: 'normal',
|
||||
},
|
||||
'[class^="FloatingVerticalDivider___"]': {
|
||||
'--tw-bg-opacity': useColorModeValue('1', '.4') + '!important',
|
||||
},
|
||||
'.slate-a': {
|
||||
color: useColorModeValue('blue.500', 'blue.300'),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TextEditorToolBar
|
||||
onVariablesButtonClick={() => setIsVariableDropdownOpen(true)}
|
||||
/>
|
||||
<PlateContent
|
||||
ref={plateContentRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
style={editorStyle(useColorModeValue('white', colors.gray[850]))}
|
||||
autoFocus
|
||||
onClick={() => {
|
||||
setIsVariableDropdownOpen(false)
|
||||
}}
|
||||
onFocus={() => {
|
||||
rememberedSelection.current = null
|
||||
if (!isFirstFocus || !editor) return
|
||||
if (editor.children.length === 0) return
|
||||
selectEditor(editor, {
|
||||
edge: 'end',
|
||||
})
|
||||
setIsFirstFocus(false)
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!editor) return
|
||||
rememberedSelection.current = editor.selection
|
||||
}}
|
||||
aria-label="Text editor"
|
||||
/>
|
||||
<Popover isOpen={isVariableDropdownOpen} isLazy>
|
||||
<PopoverAnchor>
|
||||
<Flex pos="absolute" ref={varDropdownRef} />
|
||||
</PopoverAnchor>
|
||||
<Portal>
|
||||
<PopoverContent>
|
||||
<VariableSearchInput
|
||||
initialVariableId={undefined}
|
||||
onSelectVariable={handleVariableSelected}
|
||||
placeholder={t(
|
||||
'editor.blocks.bubbles.textEditor.searchVariable.placeholder'
|
||||
)}
|
||||
autoFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
</Popover>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -9,16 +9,17 @@ import {
|
||||
MARK_ITALIC,
|
||||
MARK_UNDERLINE,
|
||||
} from '@udecode/plate-basic-marks'
|
||||
import { getPluginType, usePlateEditorRef } from '@udecode/plate-core'
|
||||
import { LinkToolbarButton } from '@udecode/plate-ui-link'
|
||||
import { MarkToolbarButton } from '@udecode/plate-ui-toolbar'
|
||||
import { getPluginType, useEditorRef } from '@udecode/plate-core'
|
||||
import {
|
||||
BoldIcon,
|
||||
ItalicIcon,
|
||||
UnderlineIcon,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
LinkIcon,
|
||||
UserIcon,
|
||||
} from '@/components/icons'
|
||||
import { MarkToolbarButton } from './plate/MarkToolbarButton'
|
||||
import { LinkToolbarButton } from './plate/LinkToolbarButton'
|
||||
|
||||
type Props = {
|
||||
onVariablesButtonClick: () => void
|
||||
@ -28,7 +29,8 @@ export const TextEditorToolBar = ({
|
||||
onVariablesButtonClick,
|
||||
...props
|
||||
}: Props) => {
|
||||
const editor = usePlateEditorRef()
|
||||
const editor = useEditorRef()
|
||||
|
||||
const handleVariablesButtonMouseDown = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
@ -52,24 +54,27 @@ export const TextEditorToolBar = ({
|
||||
/>
|
||||
<span data-testid="bold-button">
|
||||
<MarkToolbarButton
|
||||
type={getPluginType(editor, MARK_BOLD)}
|
||||
nodeType={getPluginType(editor, MARK_BOLD)}
|
||||
icon={<BoldIcon />}
|
||||
aria-label="Toggle bold"
|
||||
/>
|
||||
</span>
|
||||
<span data-testid="italic-button">
|
||||
<MarkToolbarButton
|
||||
type={getPluginType(editor, MARK_ITALIC)}
|
||||
nodeType={getPluginType(editor, MARK_ITALIC)}
|
||||
icon={<ItalicIcon />}
|
||||
aria-label="Toggle italic"
|
||||
/>
|
||||
</span>
|
||||
<span data-testid="underline-button">
|
||||
<MarkToolbarButton
|
||||
type={getPluginType(editor, MARK_UNDERLINE)}
|
||||
nodeType={getPluginType(editor, MARK_UNDERLINE)}
|
||||
icon={<UnderlineIcon />}
|
||||
aria-label="Toggle underline"
|
||||
/>
|
||||
</span>
|
||||
<span data-testid="link-button">
|
||||
<LinkToolbarButton icon={<LinkIcon />} />
|
||||
<LinkToolbarButton icon={<LinkIcon />} aria-label="Add link" />
|
||||
</span>
|
||||
</HStack>
|
||||
)
|
||||
|
@ -0,0 +1,149 @@
|
||||
import React, { useRef } from 'react'
|
||||
import {
|
||||
flip,
|
||||
offset,
|
||||
UseVirtualFloatingOptions,
|
||||
} from '@udecode/plate-floating'
|
||||
import {
|
||||
LinkFloatingToolbarState,
|
||||
useFloatingLinkEdit,
|
||||
useFloatingLinkEditState,
|
||||
useFloatingLinkInsert,
|
||||
useFloatingLinkInsertState,
|
||||
useFloatingLinkUrlInput,
|
||||
} from '@udecode/plate-link'
|
||||
import { LinkIcon, UnlinkIcon } from '@/components/icons'
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
HStack,
|
||||
IconButton,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputLeftElement,
|
||||
Stack,
|
||||
} from '@chakra-ui/react'
|
||||
import { TextInputIcon } from '@/features/blocks/inputs/textInput/components/TextInputIcon'
|
||||
|
||||
const floatingOptions: UseVirtualFloatingOptions = {
|
||||
placement: 'bottom-start',
|
||||
middleware: [
|
||||
offset(12),
|
||||
flip({
|
||||
padding: 12,
|
||||
fallbackPlacements: ['bottom-end', 'top-start', 'top-end'],
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
export interface LinkFloatingToolbarProps {
|
||||
state?: LinkFloatingToolbarState
|
||||
}
|
||||
|
||||
export function LinkFloatingToolbar({ state }: LinkFloatingToolbarProps) {
|
||||
const urlInputRef = useRef<HTMLInputElement>(null)
|
||||
const insertState = useFloatingLinkInsertState({
|
||||
...state,
|
||||
floatingOptions: {
|
||||
...floatingOptions,
|
||||
...state?.floatingOptions,
|
||||
},
|
||||
})
|
||||
const {
|
||||
props: insertProps,
|
||||
ref: insertRef,
|
||||
hidden,
|
||||
textInputProps,
|
||||
} = useFloatingLinkInsert(insertState)
|
||||
|
||||
const { props } = useFloatingLinkUrlInput({
|
||||
ref: urlInputRef,
|
||||
})
|
||||
|
||||
const editState = useFloatingLinkEditState({
|
||||
...state,
|
||||
floatingOptions: {
|
||||
...floatingOptions,
|
||||
...state?.floatingOptions,
|
||||
},
|
||||
})
|
||||
const {
|
||||
props: editProps,
|
||||
ref: editRef,
|
||||
editButtonProps,
|
||||
unlinkButtonProps,
|
||||
} = useFloatingLinkEdit(editState)
|
||||
|
||||
if (hidden) return null
|
||||
|
||||
const input = (
|
||||
<Stack
|
||||
w="330px"
|
||||
bgColor="white"
|
||||
px="4"
|
||||
py="2"
|
||||
rounded="md"
|
||||
borderWidth={1}
|
||||
shadow="md"
|
||||
>
|
||||
<InputGroup>
|
||||
<InputLeftElement pointerEvents="none">
|
||||
<LinkIcon color="gray.300" />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
ref={urlInputRef}
|
||||
placeholder="Paste link"
|
||||
defaultValue={props.defaultValue}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
||||
<Divider />
|
||||
|
||||
<InputGroup>
|
||||
<InputLeftElement pointerEvents="none">
|
||||
<TextInputIcon color="gray.300" />
|
||||
</InputLeftElement>
|
||||
<Input placeholder="Text to display" {...textInputProps} />
|
||||
</InputGroup>
|
||||
</Stack>
|
||||
)
|
||||
|
||||
const editContent = editState.isEditing ? (
|
||||
input
|
||||
) : (
|
||||
<HStack
|
||||
bgColor="white"
|
||||
p="2"
|
||||
rounded="md"
|
||||
borderWidth={1}
|
||||
shadow="md"
|
||||
align="center"
|
||||
>
|
||||
<Button {...editButtonProps} size="sm">
|
||||
Edit link
|
||||
</Button>
|
||||
|
||||
<Divider orientation="vertical" h="20px" />
|
||||
|
||||
<IconButton
|
||||
icon={<UnlinkIcon />}
|
||||
aria-label="Unlink"
|
||||
size="sm"
|
||||
{...unlinkButtonProps}
|
||||
/>
|
||||
</HStack>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={insertRef} {...insertProps}>
|
||||
{input}
|
||||
</div>
|
||||
|
||||
<div ref={editRef} {...editProps}>
|
||||
{editContent}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
import { IconButton, IconButtonProps } from '@chakra-ui/react'
|
||||
import {
|
||||
useLinkToolbarButton,
|
||||
useLinkToolbarButtonState,
|
||||
} from '@udecode/plate-link'
|
||||
|
||||
type Props = IconButtonProps
|
||||
|
||||
export const LinkToolbarButton = ({ ...rest }: Props) => {
|
||||
const state = useLinkToolbarButtonState()
|
||||
const { props } = useLinkToolbarButton(state)
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant={props.pressed ? 'outline' : 'ghost'}
|
||||
colorScheme={props.pressed ? 'blue' : undefined}
|
||||
{...props}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
useMarkToolbarButton,
|
||||
useMarkToolbarButtonState,
|
||||
} from '@udecode/plate-common'
|
||||
import { IconButton, IconButtonProps } from '@chakra-ui/react'
|
||||
|
||||
type Props = {
|
||||
nodeType: string
|
||||
clear?: string | string[]
|
||||
} & IconButtonProps
|
||||
|
||||
export const MarkToolbarButton = ({ clear, nodeType, ...rest }: Props) => {
|
||||
const state = useMarkToolbarButtonState({ clear, nodeType })
|
||||
const { props } = useMarkToolbarButton(state)
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant={props.pressed ? 'outline' : 'ghost'}
|
||||
colorScheme={props.pressed ? 'blue' : undefined}
|
||||
{...props}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { LinkFloatingToolbar } from '@/features/blocks/bubbles/textBubble/components/plate/LinkFloatingInput'
|
||||
import {
|
||||
createBoldPlugin,
|
||||
createItalicPlugin,
|
||||
@ -5,13 +6,13 @@ import {
|
||||
} from '@udecode/plate-basic-marks'
|
||||
import { createPlugins } from '@udecode/plate-core'
|
||||
import { createLinkPlugin, ELEMENT_LINK } from '@udecode/plate-link'
|
||||
import { PlateFloatingLink } from '@udecode/plate-ui-link'
|
||||
|
||||
export const editorStyle = (backgroundColor: string): React.CSSProperties => ({
|
||||
flex: 1,
|
||||
padding: '1rem',
|
||||
backgroundColor,
|
||||
borderRadius: '0.25rem',
|
||||
outline: 'none',
|
||||
})
|
||||
|
||||
export const platePlugins = createPlugins(
|
||||
@ -20,7 +21,8 @@ export const platePlugins = createPlugins(
|
||||
createItalicPlugin(),
|
||||
createUnderlinePlugin(),
|
||||
createLinkPlugin({
|
||||
renderAfterEditable: PlateFloatingLink,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
renderAfterEditable: LinkFloatingToolbar as any,
|
||||
options: {
|
||||
isUrl: (url: string) =>
|
||||
url.startsWith('http:') ||
|
||||
|
@ -19460,6 +19460,37 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"progressBar": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"isEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"backgroundColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"placement": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Top",
|
||||
"Bottom"
|
||||
]
|
||||
},
|
||||
"thickness": {
|
||||
"type": "number"
|
||||
},
|
||||
"position": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"fixed",
|
||||
"absolute"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1380,6 +1380,10 @@
|
||||
}
|
||||
},
|
||||
"description": "If the typebot contains dynamic avatars, dynamicTheme returns the new avatar URLs whenever their variables are updated."
|
||||
},
|
||||
"progress": {
|
||||
"type": "number",
|
||||
"description": "If progress bar is enabled, this field will return a number between 0 and 100 indicating the current progress based on the longest remaining path of the flow."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -1709,6 +1713,10 @@
|
||||
}
|
||||
},
|
||||
"description": "If the typebot contains dynamic avatars, dynamicTheme returns the new avatar URLs whenever their variables are updated."
|
||||
},
|
||||
"progress": {
|
||||
"type": "number",
|
||||
"description": "If progress bar is enabled, this field will return a number between 0 and 100 indicating the current progress based on the longest remaining path of the flow."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -2127,6 +2135,10 @@
|
||||
}
|
||||
},
|
||||
"description": "If the typebot contains dynamic avatars, dynamicTheme returns the new avatar URLs whenever their variables are updated."
|
||||
},
|
||||
"progress": {
|
||||
"type": "number",
|
||||
"description": "If progress bar is enabled, this field will return a number between 0 and 100 indicating the current progress based on the longest remaining path of the flow."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -6759,6 +6771,37 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"progressBar": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"isEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"backgroundColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"placement": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Top",
|
||||
"Bottom"
|
||||
]
|
||||
},
|
||||
"thickness": {
|
||||
"type": "number"
|
||||
},
|
||||
"position": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"fixed",
|
||||
"absolute"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
Reference in New Issue
Block a user