| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- 'use client'
- import Field, { getValidationRules } from './_field'
- import { useFormContext } from 'react-hook-form'
- import { useFieldController } from 'app/lib/form/use-form-field'
- import { useEffect, useMemo, useRef, useState } from 'react'
- import { EnrichedMarkdownTextInput } from 'react-native-enriched-markdown'
- import { NeoButton } from 'app/design/controls'
- import { View } from 'app/design/view'
- import { useTheme, useThemeValue } from 'app/design/theme'
- import { useCSSVariable } from 'uniwind'
- import { appSetting, cn } from 'app/lib/util'
- import emitter, { EVENTS } from 'app/context/emitter'
- import { useTranslation } from 'react-i18next'
- const inputSettings = appSetting('theme', 'inputs')
- /**
- * Wiki Markdown form field (native) — EnrichedMarkdownTextInput.
- * Web resolves `editor-markdown.web.js` (@mdxeditor/editor).
- */
- export default function FormFieldMarkdownEditor(props) {
- const { t } = useTranslation()
- const formContext = useFormContext()
- const placeholder = props.use_caption_as_placeholder
- ? props.caption
- : props.placeholder
- const isCommentsForm = props.container_class === 'comments'
- const initialHeight = props.height || (isCommentsForm ? 48 : 120)
- const rules = useMemo(() => getValidationRules(props), [
- props.checker,
- props.required,
- props.name,
- ])
- const disabled =
- props?.attrs?.readonly == 'readonly' ||
- props?.attrs?.disabled == 'disabled' ||
- !!props.disabled
- return (
- <Field {...props} error2={formContext.formState.errors[props.name]}>
- <MarkdownTextInput
- name={props.name}
- value={props.value}
- bg={props.bg}
- disabled={disabled}
- autofocus={props.autofocus}
- onFocus={props.onFocus}
- placeholder={placeholder}
- initialHeight={initialHeight}
- maxHeight={props.maxHeight || 400}
- showToolbar={props.showToolbar !== false}
- rules={rules}
- />
- </Field>
- )
- }
- export function MarkdownTextInput({
- name,
- value: valueProp,
- initialHeight = 120,
- maxHeight = 300,
- onFocus,
- bg,
- placeholder,
- autofocus,
- disabled,
- showToolbar = true,
- rules = {},
- }) {
- const { t } = useTranslation()
- const externalValue = valueProp ?? ''
- const { field } = useFieldController({
- name,
- rules,
- defaultValue: externalValue,
- })
- const { setValue } = useFormContext()
- const editorRef = useRef(null)
- const { colors } = useTheme()
- const [styleState, setStyleState] = useState(null)
- const [foregroundToken] = useCSSVariable(['--color-card-foreground'])
- const initialDefaultValue = useRef(externalValue)
- const lastWrittenRef = useRef(externalValue)
- useEffect(() => {
- if (externalValue === lastWrittenRef.current) return
- lastWrittenRef.current = externalValue
- setValue(name, externalValue)
- editorRef.current?.setValue?.(externalValue)
- }, [externalValue, name, setValue])
- useEffect(() => {
- const sub = emitter.addListener(EVENTS.editor, (data) => {
- if (!editorRef.current) return
- if (data.action === 'focus') {
- editorRef.current.focus()
- }
- if (data.action === 'set_content') {
- const next = data.value || ''
- lastWrittenRef.current = next
- editorRef.current.setValue(next)
- field.onChange(next)
- }
- })
- return () => sub.remove()
- }, [field])
- const onMarkdownChange = (markdown) => {
- const next = markdown ?? ''
- lastWrittenRef.current = next
- field.onChange(next)
- }
- const surfaceClassName =
- bg === 'transparent'
- ? 'flex-auto overflow-hidden'
- : cn(
- 'flex-auto overflow-hidden',
- inputSettings.base,
- inputSettings.size.regular,
- )
- const editorTextColor = foregroundToken || colors.default
- const linkColor = useThemeValue('rgba(37, 99, 235, 1)', 'rgba(59, 130, 246, 1)')
- const markdownStyle = useMemo(
- () => ({
- strong: { color: editorTextColor },
- em: { color: editorTextColor },
- link: { color: linkColor, underline: true },
- }),
- [editorTextColor, linkColor],
- )
- return (
- <View className={surfaceClassName}>
- <EnrichedMarkdownTextInput
- ref={editorRef}
- defaultValue={initialDefaultValue.current}
- placeholder={placeholder}
- placeholderTextColor="rgba(120,130,145,1)"
- editable={!disabled}
- autoFocus={!!autofocus}
- multiline
- scrollEnabled
- markdownStyle={markdownStyle}
- style={{
- minHeight: initialHeight,
- maxHeight,
- color: editorTextColor,
- fontSize: 16,
- backgroundColor: 'transparent',
- }}
- onChangeMarkdown={onMarkdownChange}
- onChangeState={setStyleState}
- onFocus={() => {
- if (onFocus) onFocus()
- }}
- onBlur={field.onBlur}
- />
- {showToolbar && !disabled && styleState ? (
- <View className="mt-2 flex-row flex-wrap gap-2">
- <ToolbarButton
- label="B"
- accessibilityLabel={t('Bold')}
- active={styleState.bold?.isActive}
- onPress={() => editorRef.current?.toggleBold()}
- />
- <ToolbarButton
- label="I"
- accessibilityLabel={t('Italic')}
- active={styleState.italic?.isActive}
- onPress={() => editorRef.current?.toggleItalic()}
- />
- <ToolbarButton
- label="U"
- accessibilityLabel={t('Underline')}
- active={styleState.underline?.isActive}
- onPress={() => editorRef.current?.toggleUnderline()}
- />
- <ToolbarButton
- label="S"
- accessibilityLabel={t('Strikethrough')}
- active={styleState.strikethrough?.isActive}
- onPress={() => editorRef.current?.toggleStrikethrough()}
- />
- <ToolbarButton
- label={t('Link')}
- active={styleState.link?.isActive}
- onPress={() => {
- if (styleState.link?.isActive) {
- editorRef.current?.removeLink()
- return
- }
- editorRef.current?.insertLink('link', 'https://')
- }}
- />
- </View>
- ) : null}
- </View>
- )
- }
- function ToolbarButton({ label, accessibilityLabel, active, onPress }) {
- return (
- <NeoButton
- style="borderless"
- controlSize="mini"
- selected={!!active}
- label={label}
- accessibilityLabel={accessibilityLabel}
- haptics={false}
- onPress={onPress}
- />
- )
- }
|