Skip to content

Instantly share code, notes, and snippets.

@hungdev
Last active April 2, 2025 04:32
Show Gist options
  • Save hungdev/6695c81b0418e873a59e853b8fa88a2c to your computer and use it in GitHub Desktop.
Save hungdev/6695c81b0418e873a59e853b8fa88a2c to your computer and use it in GitHub Desktop.
react editable text

hover to show border, and click to edit, click outside to text

import React, { useState, useRef, useEffect, useCallback } from "react";

const EditableText = ({
  initialValue = "fffff",
  placeholder = "Click to edit...",
  className = "",
  onChange = () => {},
}) => {
  const [isEditing, setIsEditing] = useState(false);
  const [content, setContent] = useState(initialValue);
  const editableRef = useRef(null);

  // Handler for saving content
  const saveContent = useCallback(() => {
    if (editableRef.current) {
      const newContent = editableRef.current.innerHTML;
      setContent(newContent);
      onChange(newContent);
    }
    setIsEditing(false);
  }, [onChange]);

  // Set up and clean up event listeners
  useEffect(() => {
    const handleClickOutside = (event) => {
      if (editableRef.current && !editableRef.current.contains(event.target)) {
        saveContent();
      }
    };

    if (isEditing) {
      document.addEventListener("mousedown", handleClickOutside);
      return () =>
        document.removeEventListener("mousedown", handleClickOutside);
    }
  }, [isEditing, saveContent]);

  // Handle focus and cursor placement
  useEffect(() => {
    if (isEditing && editableRef.current) {
      editableRef.current.focus();

      // Place cursor at end
      const range = document.createRange();
      const selection = window.getSelection();
      range.selectNodeContents(editableRef.current);
      range.collapse(false);
      selection.removeAllRanges();
      selection.addRange(range);
    }
  }, [isEditing]);

  // Handle keyboard events
  const handleKeyDown = (event) => {
    // Save on Enter + Ctrl
    if (event.key === "Enter" && event.ctrlKey) {
      event.preventDefault();
      saveContent();
    }

    // Cancel on Escape
    if (event.key === "Escape") {
      setIsEditing(false);
      // Revert to last saved content
      if (editableRef.current) {
        editableRef.current.innerHTML = content;
      }
    }
  };

  return (
    <div className={`editable-container ${className}`}>
      <div
        ref={editableRef}
        contentEditable={isEditing}
        suppressContentEditableWarning={true}
        onClick={() => setIsEditing(true)}
        onKeyDown={handleKeyDown}
        onBlur={saveContent}
        style={{
          padding: "8px 12px",
          cursor: isEditing ? "text" : "pointer",
          borderWidth: "1px",
          borderStyle: "solid",
          borderColor: isEditing ? "#3b82f6" : "transparent",
          borderRadius: "4px",
          outline: "none",
          minHeight: "24px",
          transition: "border-color 0.2s ease",
        }}
        onMouseEnter={(e) => {
          if (!isEditing) {
            e.target.style.borderColor = "#d1d5db";
          }
        }}
        onMouseLeave={(e) => {
          if (!isEditing) {
            e.target.style.borderColor = "transparent";
          }
        }}
        className="editable-content"
        dangerouslySetInnerHTML={{ __html: content || placeholder }}
        aria-label={placeholder}
      />
    </div>
  );
};

export default EditableText;
<EditableText 
  initialValue="Click to edit me" 
  onChange={(newContent) => console.log('Content changed:', newContent)} 
  className="my-custom-class" 
/>

https://codesandbox.io/p/sandbox/kx8j5c?file=%2Fsrc%2FEditableText.js%3A1%2C1-110%2C1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment