diff --git a/src/App.tsx b/src/App.tsx index dcd15db..72da22e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,7 @@ import { BrowserRouter, Route, Routes } from 'react-router' import HomePage from './pages/HomePage.tsx' import { PostsService } from './model/posts/postsService.ts' import AuthorPage from './pages/AuthorPage.tsx' -import { MediaService } from './pages/mediaService.ts' +import { MediaService } from './model/mediaService.ts' function App() { const postService = new PostsService() diff --git a/src/components/FancyTextEditor.tsx b/src/components/FancyTextEditor.tsx new file mode 100644 index 0000000..6db90f9 --- /dev/null +++ b/src/components/FancyTextEditor.tsx @@ -0,0 +1,95 @@ +import { useRef, useEffect, KeyboardEvent, useState } from 'react' + +interface TextInputProps { + value: string + onInput: (value: string) => void + onKeyDown: (e: TextInputKeyDownEvent) => void + className?: string + placeholder?: string +} + +export interface TextInputKeyDownEvent { + key: string + ctrlKey: boolean + preventDefault: () => void +} + +export default function FancyTextEditor({ + value: _value, + onInput, + onKeyDown, + className, + placeholder, +}: TextInputProps) { + const divRef = useRef(null) + const [hasFocus, setHasFocus] = useState(false) + + // the contenteditable likes to slip in newlines at the bottom of our innerText + // which makes it bad to check for empty string because it might be "\n" + // so we just trim it upfront and then fogeddaboudit + const value = _value.trim() + + // The funky mechanics here are to stop the cursor from jumping back the start. + // It probably will have the cursor jump to the start if anything changes programmatically, + // which is probably unnecessary anyway + useEffect(() => { + const div = divRef.current + if (div == null) { + return + } + + if (!value && !hasFocus) { + div.innerText = placeholder ?? '' + } else if (div.innerText !== value) { + div.innerText = value + } + }, [hasFocus, placeholder, value]) + + useEffect(() => { + const div = divRef.current! + if (div == null) { + return + } + + const inputListener = () => { + onInput(div.innerText) + } + + const blurListener = () => { + setHasFocus(false) + if (!value) { + div.innerText = placeholder ?? '' + } + } + + const focusListener = () => { + setHasFocus(true) + div.innerText = value + } + + div.addEventListener('input', inputListener) + div.addEventListener('blur', blurListener) + div.addEventListener('focus', focusListener) + + return () => { + div.removeEventListener('focus', focusListener) + div.removeEventListener('blur', blurListener) + div.removeEventListener('input', inputListener) + } + }, [onInput, placeholder, value]) + + const handleKeyDown = (e: KeyboardEvent) => { + onKeyDown(e) + } + + const textColour = value ? 'text-gray-900' : 'text-gray-500' + return ( +
+ ) +} diff --git a/src/components/NewPostWidget.tsx b/src/components/NewPostWidget.tsx index f79c80c..20d0165 100644 --- a/src/components/NewPostWidget.tsx +++ b/src/components/NewPostWidget.tsx @@ -1,4 +1,5 @@ import { useState, ChangeEvent } from 'react' +import FancyTextEditor, { TextInputKeyDownEvent } from './FancyTextEditor.tsx' interface NewPostWidgetProps { onSubmit: (content: string, media: File[]) => void @@ -15,8 +16,8 @@ export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPos const [content, setContent] = useState('') const [attachments, setAttachments] = useState([]) - const handleContentChange = (e: ChangeEvent) => { - setContent(e.target.value) + const onContentInput = (value: string) => { + setContent(value) } const handleMediaChange = (e: ChangeEvent) => { @@ -56,15 +57,21 @@ export default function NewPostWidget({ onSubmit, isSubmitting = false }: NewPos URL.revokeObjectURL(attachment.objectUrl) } + const onInputKeyDown = (e: TextInputKeyDownEvent) => { + if (e.key === 'Enter' && e.ctrlKey) { + e.preventDefault() + handleSubmit() + } + } + return (
-