Skip to content

Commit 46369a6

Browse files
committed
feat: update component methods and demo page
1 parent fdfb276 commit 46369a6

4 files changed

Lines changed: 67 additions & 111 deletions

File tree

lib/ContentEditable.tsx

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ interface ContentEditableProps {
66
placeholderClassName?: string
77
placeholder?: string
88
disabled?: boolean
9+
updatedContent?: string
910
onChange: (content: string) => void
1011
onKeyUp?: (e: React.KeyboardEvent) => void
1112
onKeyDown?: (e: React.KeyboardEvent) => void
1213
onFocus?: (e: React.FocusEvent) => void
1314
onBlur?: (e: React.FocusEvent) => void
15+
onContentExternalUpdate?: (content: string) => void
1416
}
1517

1618
const ContentEditable: React.FC<ContentEditableProps> = ({
@@ -19,26 +21,35 @@ const ContentEditable: React.FC<ContentEditableProps> = ({
1921
placeholderClassName,
2022
placeholder,
2123
disabled,
24+
updatedContent,
2225
onChange,
2326
onKeyUp,
2427
onKeyDown,
2528
onFocus,
2629
onBlur,
30+
onContentExternalUpdate,
2731
}) => {
2832
const [content, setContent] = useState("")
2933
const divRef = useRef<HTMLDivElement | null>(null)
3034

35+
useEffect(() => {
36+
if (updatedContent) {
37+
setContent(updatedContent)
38+
if (onContentExternalUpdate) onContentExternalUpdate(updatedContent)
39+
}
40+
}, [updatedContent, onContentExternalUpdate])
41+
3142
useEffect(() => {
3243
if (divRef.current) {
3344
divRef.current.style.height = "auto"
34-
// divRef.current.style.height = divRef.current.scrollHeight + "px"
3545
onChange(content)
3646
}
3747
}, [content, onChange])
3848

3949
/**
40-
* Check if the caret is on the last line of an element
41-
* Returns `false` when the caret is part of a selection
50+
* Checks if the caret is on the last line of a contenteditable element
51+
* @param element - The HTMLDivElement to check
52+
* @returns A boolean indicating whether the caret is on the last line or `false` when the caret is part of a selection
4253
*/
4354
const isCaretOnLastLine = useCallback((element: HTMLDivElement): boolean => {
4455
if (element.ownerDocument.activeElement !== element) return false
@@ -82,6 +93,10 @@ const ContentEditable: React.FC<ContentEditableProps> = ({
8293
return originalCaretRect.bottom === endOfElementRect.bottom
8394
}, [])
8495

96+
/**
97+
* Handles the caret scroll behavior based on keyboard events
98+
* @param e - The keyboard event
99+
*/
85100
const handleCaretScroll = useCallback(
86101
(e: KeyboardEvent) => {
87102
if (!divRef.current) return
@@ -136,6 +151,10 @@ const ContentEditable: React.FC<ContentEditableProps> = ({
136151
}
137152
}
138153

154+
/**
155+
* Inserts the specified text at the current caret position in the contentEditable element
156+
* @param text - The text to be inserted
157+
*/
139158
function insertTextAtCaret(text: string) {
140159
if (!divRef.current) return
141160
const currentCaretPos = getCaretPosition(divRef.current)
@@ -152,6 +171,13 @@ const ContentEditable: React.FC<ContentEditableProps> = ({
152171

153172
// Note: setSelectionRange and createTextRange are not supported by contenteditable elements
154173

174+
/**
175+
* Sets the caret position within the contentEditable element
176+
* If the element is empty, it will be focused
177+
*
178+
* @param elem - The contentEditable element
179+
* @param pos - The position to set the caret to
180+
*/
155181
function setCaretPosition(elem: HTMLElement, pos: number) {
156182
// Create a new range
157183
const range = document.createRange()
@@ -178,6 +204,11 @@ const ContentEditable: React.FC<ContentEditableProps> = ({
178204
}
179205
}
180206

207+
/**
208+
* Retrieves the caret position within the contentEditable element
209+
* @param editableDiv - The contentEditable element
210+
* @returns The caret position as a number
211+
*/
181212
function getCaretPosition(editableDiv: HTMLElement) {
182213
let caretPos = 0,
183214
range
@@ -207,7 +238,6 @@ const ContentEditable: React.FC<ContentEditableProps> = ({
207238
function handleKeyDown(e: React.KeyboardEvent) {
208239
if (onKeyDown) onKeyDown(e)
209240
if ((e.key === "Delete" || e.key === "Backspace") && isAllTextSelected()) {
210-
console.log("delete all", isAllTextSelected())
211241
e.preventDefault()
212242
if (divRef.current) {
213243
divRef.current.innerText = ""
@@ -237,13 +267,7 @@ const ContentEditable: React.FC<ContentEditableProps> = ({
237267
return (
238268
<div
239269
className={containerClassName}
240-
style={{
241-
display: "flex",
242-
justifyContent: "flex-start",
243-
alignItems: "center",
244-
width: "calc(100% - 32px)",
245-
padding: "0 16px",
246-
}}
270+
style={{ display: "flex", alignItems: "center" }}
247271
>
248272
<div
249273
ref={divRef}
@@ -255,15 +279,9 @@ const ContentEditable: React.FC<ContentEditableProps> = ({
255279
aria-label={placeholder ?? ""}
256280
className={contentEditableClassName}
257281
style={{
258-
width: "100%",
259-
border: "1px solid #ccc",
260-
borderRadius: "0.35rem",
261282
padding: "calc((1.5rem * 1.3125)/2) 0 calc((1.5rem * 1.3125)/2) 1rem",
262-
minHeight: "19px",
263-
maxHeight: "160px",
264283
overflow: "auto",
265284
height: "auto",
266-
lineHeight: 1.3,
267285
textAlign: "initial",
268286
wordBreak: "break-word",
269287
unicodeBidi: "plaintext",
@@ -297,10 +315,8 @@ const ContentEditable: React.FC<ContentEditableProps> = ({
297315
className={placeholderClassName}
298316
style={{
299317
position: "absolute",
300-
color: "#a2acb4",
301318
pointerEvents: "none",
302319
textAlign: "initial",
303-
marginLeft: "1rem",
304320
}}
305321
>
306322
{placeholder ?? ""}

src/App.css

Lines changed: 23 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,24 @@
1-
#root {
2-
max-width: 1280px;
3-
margin: 0 auto;
4-
padding: 2rem;
5-
text-align: center;
6-
}
7-
8-
.logo {
9-
height: 6em;
10-
padding: 1.5em;
11-
will-change: filter;
12-
transition: filter 300ms;
13-
}
14-
.logo:hover {
15-
filter: drop-shadow(0 0 2em #646cffaa);
16-
}
17-
.logo.react:hover {
18-
filter: drop-shadow(0 0 2em #61dafbaa);
19-
}
20-
21-
@keyframes logo-spin {
22-
from {
23-
transform: rotate(0deg);
24-
}
25-
to {
26-
transform: rotate(360deg);
27-
}
28-
}
29-
30-
@media (prefers-reduced-motion: no-preference) {
31-
a:nth-of-type(2) .logo {
32-
animation: logo-spin infinite 20s linear;
33-
}
34-
}
35-
36-
.card {
37-
padding: 2em;
38-
}
39-
40-
.read-the-docs {
41-
color: #888;
1+
.main-container {
2+
padding: 3rem;
3+
display: flex;
4+
justify-content: center;
5+
align-items: center;
6+
}
7+
.full-width {
8+
width: 100%;
9+
}
10+
.input-container {
11+
padding: 0 1rem;
12+
position: relative;
13+
}
14+
.input-element {
15+
border: 1px solid #ccc;
16+
border-radius: 0.35rem;
17+
line-height: 1.3;
18+
min-height: 1.188rem;
19+
max-height: 10rem;
20+
}
21+
.input-placeholder {
22+
color: #a2acb4;
23+
margin-left: 1rem;
4224
}

src/App.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ import { useState } from "react"
55
function App() {
66
const [content, setContent] = useState("")
77
return (
8-
<div>
8+
<div className="main-container full-width">
99
<div style={{ whiteSpace: "pre-wrap" }}>{content}</div>
1010
<ContentEditable
1111
placeholder="Type here"
12+
containerClassName="full-width input-container"
13+
contentEditableClassName="full-width input-element"
14+
placeholderClassName="input-placeholder"
1215
onChange={(content) => setContent(content)}
1316
/>
1417
</div>

src/index.css

Lines changed: 5 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,12 @@
11
:root {
22
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3-
line-height: 1.5;
43
font-weight: 400;
5-
6-
color-scheme: light dark;
7-
color: rgba(255, 255, 255, 0.87);
8-
background-color: #242424;
9-
104
font-synthesis: none;
115
text-rendering: optimizeLegibility;
126
-webkit-font-smoothing: antialiased;
137
-moz-osx-font-smoothing: grayscale;
148
}
159

16-
a {
17-
font-weight: 500;
18-
color: #646cff;
19-
text-decoration: inherit;
20-
}
21-
a:hover {
22-
color: #535bf2;
23-
}
24-
2510
body {
2611
margin: 0;
2712
display: flex;
@@ -30,39 +15,9 @@ body {
3015
min-height: 100vh;
3116
}
3217

33-
h1 {
34-
font-size: 3.2em;
35-
line-height: 1.1;
36-
}
37-
38-
button {
39-
border-radius: 8px;
40-
border: 1px solid transparent;
41-
padding: 0.6em 1.2em;
42-
font-size: 1em;
43-
font-weight: 500;
44-
font-family: inherit;
45-
background-color: #1a1a1a;
46-
cursor: pointer;
47-
transition: border-color 0.25s;
48-
}
49-
button:hover {
50-
border-color: #646cff;
51-
}
52-
button:focus,
53-
button:focus-visible {
54-
outline: 4px auto -webkit-focus-ring-color;
55-
}
56-
57-
@media (prefers-color-scheme: light) {
58-
:root {
59-
color: #213547;
60-
background-color: #ffffff;
61-
}
62-
a:hover {
63-
color: #747bff;
64-
}
65-
button {
66-
background-color: #f9f9f9;
67-
}
18+
#root {
19+
width: 100%;
20+
display: flex;
21+
justify-content: center;
22+
align-items: center;
6823
}

0 commit comments

Comments
 (0)