Skip to content

Commit

Permalink
chore: language selector for code block
Browse files Browse the repository at this point in the history
  • Loading branch information
petyosi committed May 5, 2023
1 parent 0536ada commit 4068506
Show file tree
Hide file tree
Showing 12 changed files with 219 additions and 91 deletions.
1 change: 0 additions & 1 deletion src/system/Editor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { $createCodeNode } from '@lexical/code'
import {
$isListNode,
INSERT_ORDERED_LIST_COMMAND,
Expand Down
10 changes: 2 additions & 8 deletions src/system/Sandpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,7 @@ export const [SandpackSystem] = system(

$insertNodeToNearestRoot(sandpackNode)
// TODO: hack, decoration is not synchronous ;(
setTimeout(() => {
sandpackNode.select()
r.pub(activeEditorType, { type: 'sandpack', nodeKey: sandpackNode.getKey() })
}, 100)
setTimeout(() => sandpackNode.select(), 80)
})
}
}
Expand All @@ -135,10 +132,7 @@ export const [SandpackSystem] = system(

$insertNodeToNearestRoot(codeBlockNode)
// TODO: hack, decoration is not synchronous ;(
setTimeout(() => {
codeBlockNode.select()
r.pub(activeEditorType, { type: 'codeblock', nodeKey: codeBlockNode.getKey() })
}, 100)
setTimeout(() => codeBlockNode.select(), 80)
})
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/types/ActiveEditorType.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
type LexicalEditorType = {
export type LexicalEditorType = {
type: 'lexical'
}

type CodeBlockEditorType = {
export type CodeBlockEditorType = {
type: 'codeblock'
nodeKey: string
}

type SandpackEditorType = {
export type SandpackEditorType = {
type: 'sandpack'
nodeKey: string
}
Expand Down
13 changes: 8 additions & 5 deletions src/ui/NodeDecorators/CodeBlockEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,27 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
import React from 'react'
import { CodeBlockEditorProps } from '../../types/NodeDecoratorsProps'
import { useCodeMirrorRef } from './useCodeMirrorRef'
import { useEmitterValues, usePublisher } from '../../system'

export const CodeBlockEditor = ({ nodeKey, code, language, onChange, focusEmitter }: CodeBlockEditorProps) => {
const [editor] = useLexicalComposerContext()
const codeMirrorRef = useCodeMirrorRef(nodeKey, 'codeblock')
const [activeEditor] = useEmitterValues('activeEditor')
const codeMirrorRef = useCodeMirrorRef(nodeKey, 'codeblock', language)
const setActiveEditorType = usePublisher('activeEditorType')

React.useEffect(() => {
focusEmitter.subscribe(() => {
codeMirrorRef?.current?.getCodemirror()?.focus()
setActiveEditorType({ type: 'codeblock', nodeKey })
})
}, [focusEmitter, codeMirrorRef])
}, [focusEmitter, codeMirrorRef, setActiveEditorType, nodeKey])

const wrappedOnChange = React.useCallback(
(code: string) => {
editor.update(() => {
activeEditor.update(() => {
onChange(code)
})
},
[onChange, editor]
[onChange, activeEditor]
)

return (
Expand Down
8 changes: 5 additions & 3 deletions src/ui/NodeDecorators/SandpackEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SandpackCodeEditor, SandpackLayout, SandpackPreview, SandpackProvider, useSandpack } from '@codesandbox/sandpack-react'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import React from 'react'
import { useEmitterValues } from '../../system'
import { useEmitterValues, usePublisher } from '../../system'
import { SandpackConfigValue, SandpackPreset } from '../../system/Sandpack'
import { SandpackEditorProps } from '../../types/NodeDecoratorsProps'
import { parseCodeBlockMeta } from './parseCodeBlockMeta'
Expand Down Expand Up @@ -36,13 +36,15 @@ function getPreset(meta: string, config: SandpackConfigValue) {
export const SandpackEditor = ({ nodeKey, code, meta, onChange, focusEmitter }: SandpackEditorProps) => {
const [editor] = useLexicalComposerContext()
const [config] = useEmitterValues('sandpackConfig')
const codeMirrorRef = useCodeMirrorRef(nodeKey, 'sandpack')
const codeMirrorRef = useCodeMirrorRef(nodeKey, 'sandpack', 'jsx')
const setActiveEditorType = usePublisher('activeEditorType')

React.useEffect(() => {
focusEmitter.subscribe(() => {
codeMirrorRef?.current?.getCodemirror()?.focus()
setActiveEditorType({ type: 'sandpack', nodeKey })
})
}, [focusEmitter, codeMirrorRef])
}, [focusEmitter, codeMirrorRef, setActiveEditorType, nodeKey])

const wrappedOnChange = React.useCallback(
(code: string) => {
Expand Down
14 changes: 7 additions & 7 deletions src/ui/NodeDecorators/useCodeMirrorRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { CodeMirrorRef } from '@codesandbox/sandpack-react/dist/components/CodeE
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { $createParagraphNode, $getNodeByKey } from 'lexical'
import React from 'react'
import { usePublisher } from '../../system'
import { useEmitterValues, usePublisher } from '../../system'

export function useCodeMirrorRef(nodeKey: string, editorType: 'codeblock' | 'sandpack') {
const [editor] = useLexicalComposerContext()
export function useCodeMirrorRef(nodeKey: string, editorType: 'codeblock' | 'sandpack', language: string) {
const [activeEditor] = useEmitterValues('activeEditor')
const setActiveEditorType = usePublisher('activeEditorType')
const codeMirrorRef = React.useRef<CodeMirrorRef>(null)

Expand All @@ -22,7 +22,7 @@ export function useCodeMirrorRef(nodeKey: string, editorType: 'codeblock' | 'san
const selectionEnd = state.selection.ranges[0].to

if (docLength === selectionEnd) {
editor.update(() => {
activeEditor.update(() => {
const node = $getNodeByKey(nodeKey)!
const nextSibling = node.getNextSibling()
if (nextSibling) {
Expand All @@ -40,7 +40,7 @@ export function useCodeMirrorRef(nodeKey: string, editorType: 'codeblock' | 'san
const selectionStart = state.selection.ranges[0].from

if (selectionStart === 0) {
editor.update(() => {
activeEditor.update(() => {
const node = $getNodeByKey(nodeKey)!
const previousSibling = node.getPreviousSibling()
if (previousSibling) {
Expand All @@ -56,7 +56,7 @@ export function useCodeMirrorRef(nodeKey: string, editorType: 'codeblock' | 'san
e.stopPropagation()
}
},
[editor, nodeKey]
[activeEditor, nodeKey]
)

React.useEffect(() => {
Expand All @@ -73,7 +73,7 @@ export function useCodeMirrorRef(nodeKey: string, editorType: 'codeblock' | 'san
codeMirror?.getCodemirror()?.contentDOM.removeEventListener('focus', onFocusHandler)
codeMirror?.getCodemirror()?.contentDOM.removeEventListener('keydown', onKeyDownHandler)
}
}, [codeMirrorRef, onFocusHandler, onKeyDownHandler])
}, [codeMirrorRef, onFocusHandler, onKeyDownHandler, language])

return codeMirrorRef
}
70 changes: 16 additions & 54 deletions src/ui/ToolbarPlugin/BlockTypeSelect/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import React from 'react'
import * as Select from '@radix-ui/react-select'
import { ReactComponent as DropDownIcon } from '../icons/arrow_drop_down.svg'
import { ReactComponent as SelectedIcon } from '../icons/check_small.svg'
import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'
import { AdmonitionKind } from '../../../nodes'
import { useEmitterValues, usePublisher } from '../../../system'
import classnames from 'classnames'
import { SelectItem, SelectTrigger, SelectContent } from '../SelectPieces'

export type HeadingType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
export type BlockType = 'paragraph' | 'code' | 'quote' | HeadingType
Expand All @@ -19,56 +16,21 @@ export const BlockTypeSelect = () => {
const applyBlockType = usePublisher('applyBlockType')
return (
<Select.Root value={currentBlockType || ('' as const)} onValueChange={applyBlockType as (value: string) => void}>
<Select.Trigger
aria-label="Block type"
className="group flex w-36 flex-row items-center rounded-md border-0 bg-transparent p-0 pl-4 pr-2 font-sans text-base hover:bg-primary-100 data-[state=open]:rounded-b-none data-[state=open]:bg-primary-100"
>
<Select.Value placeholder="Block type" />
<Select.Icon className="ml-auto group-data-[state=open]:text-primary-900 [&_svg]:block">
<DropDownIcon />
</Select.Icon>
</Select.Trigger>
<Select.Portal className="font-sans">
<Select.Content className="w-36 rounded-b-md bg-primary-100" onCloseAutoFocus={(e) => e.preventDefault()} position="popper">
<Select.ScrollUpButton className="">
<ChevronUpIcon />
</Select.ScrollUpButton>
<Select.Viewport className="">
<SelectItem value="paragraph">Paragraph</SelectItem>
<SelectItem value="code">Code Block</SelectItem>
<SelectItem value="quote">Quote</SelectItem>
<Select.Separator />
<SelectItem value="h1">Heading 1</SelectItem>
<SelectItem value="h2">Heading 2</SelectItem>
<SelectItem value="h3">Heading 3</SelectItem>
<SelectItem value="h4">Heading 4</SelectItem>
<SelectItem value="h5">Heading 5</SelectItem>
<SelectItem value="h6">Heading 6</SelectItem>
<Select.Separator />
<SelectItem value="info">Info</SelectItem>
</Select.Viewport>
<Select.ScrollDownButton className="">
<ChevronDownIcon />
</Select.ScrollDownButton>
</Select.Content>
</Select.Portal>
<SelectTrigger placeholder="Block type" />
<SelectContent>
<SelectItem value="paragraph">Paragraph</SelectItem>
<SelectItem value="code">Code Block</SelectItem>
<SelectItem value="quote">Quote</SelectItem>
<Select.Separator />
<SelectItem value="h1">Heading 1</SelectItem>
<SelectItem value="h2">Heading 2</SelectItem>
<SelectItem value="h3">Heading 3</SelectItem>
<SelectItem value="h4">Heading 4</SelectItem>
<SelectItem value="h5">Heading 5</SelectItem>
<SelectItem value="h6">Heading 6</SelectItem>
<Select.Separator />
<SelectItem value="info">Info</SelectItem>
</SelectContent>
</Select.Root>
)
}

const SelectItem = React.forwardRef<HTMLDivElement | null, { className?: string; children: React.ReactNode; value: string }>(
({ children, className, ...props }, forwardedRef) => {
return (
<Select.Item
{...props}
ref={forwardedRef}
className={classnames(
className,
`cursor-default px-4 py-2 data-[highlighted]:bg-primary-200 data-[state=checked]:bg-primary-300 data-[highlighted]:outline-0 flex`
)}
>
<Select.ItemText>{children}</Select.ItemText>
</Select.Item>
)
}
)
44 changes: 44 additions & 0 deletions src/ui/ToolbarPlugin/CodeBlockLanguageSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react'
import * as Select from '@radix-ui/react-select'
// import { useEmitterValues, usePublisher } from '../../../system'
import { SelectItem, SelectTrigger, SelectContent } from './SelectPieces'
import { useEmitterValues } from '..'
import { $getNodeByKey } from 'lexical'
import { CodeBlockEditorType } from '../../types/ActiveEditorType'
import { CodeBlockNode } from '../../nodes/CodeBlock'

export function CodeBlockLanguageSelect() {
const [activeEditorType, activeEditor] = useEmitterValues('activeEditorType', 'activeEditor')

console.log(activeEditorType)
const nodeLanguage = React.useMemo(() => {
let language!: string
activeEditor!.getEditorState().read(() => {
const node = $getNodeByKey((activeEditorType as CodeBlockEditorType).nodeKey) as CodeBlockNode
language = node.getLanguage()
})
return language
}, [activeEditor, activeEditorType])

return (
<Select.Root
value={nodeLanguage}
onValueChange={(language) => {
activeEditor!.update(() => {
const node = $getNodeByKey((activeEditorType as CodeBlockEditorType).nodeKey) as CodeBlockNode
node.setLanguage(language)
setTimeout(() => {
node.select()
}, 80)
})
}}
>
<SelectTrigger placeholder="Language" />
<SelectContent>
<SelectItem value="js">JavaScript</SelectItem>
<SelectItem value="ts">TypeScript</SelectItem>
<SelectItem value="css">CSS</SelectItem>
</SelectContent>
</Select.Root>
)
}
51 changes: 51 additions & 0 deletions src/ui/ToolbarPlugin/SelectPieces.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react'
import * as Select from '@radix-ui/react-select'
import { ReactComponent as DropDownIcon } from './icons/arrow_drop_down.svg'
import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'
import classnames from 'classnames'

export const SelectItem = React.forwardRef<HTMLDivElement | null, { className?: string; children: React.ReactNode; value: string }>(
({ children, className, ...props }, forwardedRef) => {
return (
<Select.Item
{...props}
ref={forwardedRef}
className={classnames(
className,
`cursor-default px-4 py-2 data-[highlighted]:bg-primary-200 data-[state=checked]:bg-primary-300 data-[highlighted]:outline-0 flex`
)}
>
<Select.ItemText>{children}</Select.ItemText>
</Select.Item>
)
}
)
export function SelectTrigger({ placeholder }: { placeholder: string }) {
return (
<Select.Trigger
aria-label={placeholder}
className="group flex w-36 flex-row items-center rounded-md border-0 bg-transparent p-2 pl-4 pr-2 font-sans text-base hover:bg-primary-100 data-[state=open]:rounded-b-none data-[state=open]:bg-primary-100"
>
<Select.Value placeholder={placeholder} />
<Select.Icon className="ml-auto group-data-[state=open]:text-primary-900 [&_svg]:block">
<DropDownIcon />
</Select.Icon>
</Select.Trigger>
)
}

export function SelectContent({ children }: { children: React.ReactNode }) {
return (
<Select.Portal className="font-sans">
<Select.Content className="w-36 rounded-b-md bg-primary-100" onCloseAutoFocus={(e) => e.preventDefault()} position="popper">
<Select.ScrollUpButton className="">
<ChevronUpIcon />
</Select.ScrollUpButton>
<Select.Viewport className="">{children}</Select.Viewport>
<Select.ScrollDownButton className="">
<ChevronDownIcon />
</Select.ScrollDownButton>
</Select.Content>
</Select.Portal>
)
}
8 changes: 8 additions & 0 deletions src/ui/ToolbarPlugin/icons/delete.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 4068506

Please sign in to comment.