Skip to content

Commit

Permalink
猬嗭笍 Upgrade and improve plate editor
Browse files Browse the repository at this point in the history
Closes #606
  • Loading branch information
baptisteArno committed Feb 27, 2024
1 parent ce17ce5 commit b9e5468
Show file tree
Hide file tree
Showing 18 changed files with 689 additions and 1,502 deletions.
18 changes: 8 additions & 10 deletions apps/builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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:*",
Expand Down
11 changes: 11 additions & 0 deletions apps/builder/src/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
)
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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={
Expand All @@ -193,11 +31,7 @@ export const TextBubbleEditor = ({
}
onChange={setTextEditorValue}
>
<TextBubbleEditorContent
id={id}
textEditorValue={textEditorValue}
onClose={onClose}
/>
</PlateProvider>
<TextEditorEditorContent closeEditor={closeEditor} />
</Plate>
)
}

0 comments on commit b9e5468

Please sign in to comment.