import { useEffect, useState, useCallback } from 'react'
import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button, TextField, IconButton, Box, Grid } from '@mui/material'
import { Dehaze, Save as SaveIcon, Clear as ClearIcon } from '@mui/icons-material'
import { useForm, useFieldArray, UseFormRegister, FieldErrors, SubmitHandler } from 'react-hook-form'
import { DndContext, closestCenter, DragEndEvent, DragMoveEvent, useSensor, useSensors, TouchSensor, MouseSensor, DragStartEvent, useDroppable, UniqueIdentifier } from '@dnd-kit/core'
import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
export interface MessageFormInputs {
messages: { id: number, value: string }[]
}
export default function CustomMessageEditor() {
const [isOpen, setIsOpen] = useState(false)
const [activeId, setActiveId] = useState<string | null>(null)
const [isFocused, setIsFocused] = useState(false)
const [idOfOverDeleteArea, setIdOfOverDeleteArea] = useState<string | null>(null)
const { control, register, handleSubmit, watch, setValue, reset, formState: { isDirty, errors, isValid } } = useForm<MessageFormInputs>({ mode: 'all'})
const { fields, append, remove, move } = useFieldArray({ control, name: 'messages' })
const sensors = useSensors(
useSensor(MouseSensor),
useSensor(TouchSensor, {
activationConstraint: {
delay: 0,
tolerance: 10,
},
})
)
const handleDragStart = (event: DragStartEvent) => {
setActiveId(event.active.id as string)
}
const handleDragMove = (event: DragMoveEvent) => {
const { active, over } = event
if (over && over.id === 'delete-area') {
setIdOfOverDeleteArea(active.id as string)
} else {
setIdOfOverDeleteArea(null)
}
}
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event
if (!over) {
setActiveId(null)
setIdOfOverDeleteArea(null)
return
}
if (over.id === 'delete-area') {
const index = fields.findIndex(field => field.id === active.id)
remove(index)
} else if (active.id !== over.id) {
const oldIndex = fields.findIndex(field => field.id === active.id)
const newIndex = fields.findIndex(field => field.id === over.id)
move(oldIndex, newIndex)
}
setActiveId(null)
setIdOfOverDeleteArea(null)
}
const load = () => {
const messagesJSON = localStorage.getItem('messages')
if(messagesJSON) {
const messages = (JSON.parse(messagesJSON) as string[]).map((value, id) => ({id, value}))
return {messages} as MessageFormInputs
}
return {messages: []} as MessageFormInputs
}
useEffect(()=>{
reset(load())
}, [])
const save: SubmitHandler<MessageFormInputs> = useCallback(async(form) => {
const json = JSON.stringify(form.messages.map(message => message.value))
localStorage.setItem('messages', json)
setIsOpen(false)
}, [])
const cancel = () => {
setIsOpen(false)
setTimeout(()=> reset(load()), 200)
}
return (
<>
<Button onClick={() => setIsOpen(true)}>テンプレートメッセージ設定</Button>
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
<DialogTitle>
テンプレートメッセージ設定
</DialogTitle>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragStart={handleDragStart} onDragEnd={handleDragEnd} onDragMove={handleDragMove}>
<DialogContent sx={{pb: 0, overflow: activeId ? 'hidden' : 'auto'}}>
<DialogContentText>
タブレットに表示するオプションメッセージ表示をカスタマイズ出来ます。空欄は使用できません。
</DialogContentText>
<SortableContext items={fields.map(field => field.id)} strategy={verticalListSortingStrategy}>
{fields.map((field, index) => (
<SortableItem key={field.id} id={field.id} index={index} register={register} value={field.value} isFocused={isFocused} setIsFocused={setIsFocused} isOverDeleteArea={idOfOverDeleteArea == field.id} isError={!!errors.messages?.[index]} />
))}
</SortableContext>
</DialogContent>
<DeleteArea isDragging={!!activeId} onClick={() => append({ id: fields.length, value: '' })} />
</DndContext>
<DialogActions>
<Button onClick={cancel} autoFocus>キャンセル</Button>
<Button onClick={handleSubmit(save)} variant="contained" disabled={!isValid || !isDirty}>保存して終了</Button>
</DialogActions>
</Dialog>
</>
)
}
interface SortableItemProps {
id: string
index: number
register: UseFormRegister<MessageFormInputs>
value: string
isFocused: boolean
setIsFocused: (focused: boolean) => void
isOverDeleteArea: boolean
isError: boolean
}
function SortableItem({ id, index, register, value, isFocused, setIsFocused, isOverDeleteArea, isError }: SortableItemProps) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id })
const style = {
transform: CSS.Transform.toString({
x: 0, // 水平方向の移動を制限
y: transform?.y ?? 0, // yがundefinedの場合に0を代入
scaleX: transform?.scaleX ?? 1,
scaleY: transform?.scaleY ?? 1,
}),
transition,
marginBottom: '8px',
}
const { onBlur, ...inputProps } = register(`messages.${index}.value`, { required: true })
return (
<div ref={setNodeRef} style={style} {...attributes}>
<Box display="flex" alignItems="center">
<TextField
fullWidth
sx={{ mt: 1, ml: 1, '& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: isOverDeleteArea || isError ? 'red' : 'grey', // 未フォーカス時のボーダーラインの色変更
},
}}}
InputProps={{style: isOverDeleteArea ? {color: 'red'}:{}}}
{...inputProps}
onFocus={() => setIsFocused(true)}
onBlur={(e) => {
onBlur(e)
setIsFocused(false)
}}
defaultValue={value}
error={isError}
/>
<IconButton {...(isFocused ? {} : listeners)} sx={{ color: isFocused ? 'lightgray' : 'inherit', touchAction: 'none', mt:1 }}>
<Dehaze sx={isOverDeleteArea ? {color: 'red'}:{}} />
</IconButton>
</Box>
</div>
)
}
function DeleteArea({isDragging, onClick}: {isDragging: boolean, onClick: () => void}) {
const { setNodeRef, isOver } = useDroppable({
id: 'delete-area'
})
return (
<Box ref={setNodeRef} display="flex" justifyContent="space-between" sx={{mr: 4, ml: 4, mt: 1, mb: 2}} >
<Button onClick={onClick} variant="outlined">追加</Button>
<Box sx={{textAlign: 'center', border: `1px ${isOver ? 'solid' : 'dashed'} ${isOver ? 'red' : isDragging ? 'gray' : 'lightgrey'}`, padding: '5px 20px', color: isOver ? 'red' : isDragging ? 'gray' : 'lightgrey', borderRadius: 2}}>
削除
</Box>
</Box>
)
}