react-text-editor-kit
Version:
React Text Editor Kit is a customizable rich text editor component for React applications. It provides a flexible and easy-to-use interface for users to create and edit content with various formatting options. And also easy to integrate in react applicati
317 lines (282 loc) • 9.59 kB
JSX
import React, { useEffect, useRef, useState } from "react";
import Styles from "../css/style.module.css";
import ArrowDown from "./SVGImages/ArrowDown";
function LineHeight({ editorRef, isDisable }) {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState("default");
const selectRef = useRef(null);
const lineHeights = [
{ label: "Default", value: "default" },
{ label: "1.0", value: "1" },
{ label: "1.1", value: "1.1" },
{ label: "1.2", value: "1.2" },
{ label: "1.3", value: "1.3" },
{ label: "1.4", value: "1.4" },
{ label: "1.5", value: "1.5" },
{ label: "1.6", value: "1.6" },
{ label: "1.8", value: "1.8" },
{ label: "2.0", value: "2" },
{ label: "2.5", value: "2.5" },
{ label: "3.0", value: "3" },
];
const toggleSelect = (e) => {
if (isDisable) {
return;
}
e.preventDefault();
setIsOpen(!isOpen);
};
const handleOptionClick = (e, option) => {
e.preventDefault();
editorRef.current.focus();
const selection = window.getSelection();
if (!selection.rangeCount) return;
const range = selection.getRangeAt(0);
const value = option.value;
// Function to find the parent block element
const findParentBlockElement = (node) => {
while (node && node !== editorRef.current) {
if (node.nodeType === Node.ELEMENT_NODE) {
const tagName = node.tagName.toLowerCase();
if (
[
"p",
"div",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"li",
"blockquote",
].includes(tagName)
) {
return node;
}
}
node = node.parentNode;
}
return null;
};
// Function to get all block elements within the selection
const getAllBlockElementsInSelection = () => {
const blockElements = [];
if (selection.isCollapsed) {
// If no selection, just get the current block element
const currentBlock = findParentBlockElement(range.startContainer);
if (currentBlock) {
blockElements.push(currentBlock);
}
return blockElements;
}
// Get all block elements in the editor
const allBlocks = editorRef.current.querySelectorAll(
"p, div, h1, h2, h3, h4, h5, h6, li, blockquote",
);
// Check which blocks intersect with the selection
allBlocks.forEach((block) => {
if (rangeIntersectsNode(range, block)) {
blockElements.push(block);
}
});
return blockElements;
};
// Helper function to check if range intersects with a node
const rangeIntersectsNode = (range, node) => {
try {
const nodeRange = document.createRange();
nodeRange.selectNodeContents(node);
// Check if ranges overlap
return (
range.compareBoundaryPoints(Range.START_TO_END, nodeRange) > 0 &&
nodeRange.compareBoundaryPoints(Range.START_TO_END, range) > 0
);
} catch (e) {
return false;
}
};
// Get all block elements that should be affected
const targetElements = getAllBlockElementsInSelection();
if (targetElements.length > 0) {
// Apply line height to all found block elements
targetElements.forEach((element) => {
element.style.lineHeight = value;
});
} else {
// If no block element found, create a paragraph wrapper
const selection = window.getSelection();
const range = selection.getRangeAt(0);
// Get the content at cursor or selection
let contentToWrap;
if (selection.isCollapsed) {
// If no selection, wrap the current text node or create new paragraph
const currentNode = range.startContainer;
if (currentNode.nodeType === Node.TEXT_NODE) {
contentToWrap = currentNode;
} else {
// Create new paragraph with line height
const p = document.createElement("p");
p.style.lineHeight = value;
p.innerHTML = "<br>";
range.insertNode(p);
// Place cursor inside the new paragraph
const newRange = document.createRange();
newRange.setStart(p, 0);
newRange.collapse(true);
selection.removeAllRanges();
selection.addRange(newRange);
setSelectedOption(option.label);
setIsOpen(false);
return;
}
} else {
contentToWrap = range.extractContents();
}
// Create paragraph wrapper with line height
const p = document.createElement("p");
p.style.lineHeight = value;
if (contentToWrap) {
p.appendChild(contentToWrap);
range.insertNode(p);
// Restore selection within the new paragraph
const newRange = document.createRange();
newRange.selectNodeContents(p);
selection.removeAllRanges();
selection.addRange(newRange);
}
}
setSelectedOption(option.label);
setIsOpen(false);
// Trigger input event to update editor state
const inputEvent = new Event("input", { bubbles: true });
editorRef.current.dispatchEvent(inputEvent);
};
const getCurrentLineHeight = () => {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const startNode = range.startContainer;
// Function to find the parent block element
const findParentBlockElement = (node) => {
while (node && node !== editorRef.current) {
if (node.nodeType === Node.ELEMENT_NODE) {
const tagName = node.tagName.toLowerCase();
if (
[
"p",
"div",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"li",
"blockquote",
].includes(tagName)
) {
return node;
}
}
node = node.parentNode;
}
return null;
};
// Look for line height in the block element first
const blockElement = findParentBlockElement(startNode);
if (blockElement && blockElement.style.lineHeight) {
const lineHeight = blockElement.style.lineHeight;
const foundHeight = lineHeights.find(
(height) => height.value === lineHeight,
);
if (foundHeight) {
return foundHeight.label;
}
}
// Fallback: check any parent element with line height
let node = startNode;
while (node && node !== editorRef.current) {
if (node.nodeType === Node.ELEMENT_NODE && node.style.lineHeight) {
const lineHeight = node.style.lineHeight;
const foundHeight = lineHeights.find(
(height) => height.value === lineHeight,
);
if (foundHeight) {
return foundHeight.label;
}
}
node = node.parentNode;
}
}
return null;
};
const handleKeyDown = () => {
const editor = editorRef.current;
if (!editor) return;
if (!editor.contains(window.getSelection().anchorNode)) {
return;
}
editor.focus();
};
useEffect(() => {
const handleClickOutside = (event) => {
if (selectRef.current && !selectRef.current.contains(event.target)) {
setIsOpen(false);
}
};
const handleSelectionChange = () => {
if (editorRef?.current?.contains(window.getSelection().anchorNode)) {
const currentHeight = getCurrentLineHeight();
if (currentHeight) {
setSelectedOption(currentHeight);
} else {
setSelectedOption("default");
}
}
};
document.addEventListener("click", handleClickOutside);
document.addEventListener("selectionchange", handleSelectionChange);
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("click", handleClickOutside);
document.removeEventListener("selectionchange", handleSelectionChange);
document.removeEventListener("keydown", handleKeyDown);
};
}, [editorRef]);
return (
<button
type="button"
className={`${Styles.customSelectFormat} ${
isDisable ? Styles.disabledButton : ""
}`}
onClick={toggleSelect}
ref={selectRef}
disabled={isDisable}
title="Line Height"
>
<div className={Styles.selectSelected}>
{selectedOption}
<ArrowDown />
</div>
{isOpen && (
<div
className={Styles.selectItemsFormat}
style={{ maxHeight: "200px", overflowY: "auto" }}
>
{lineHeights.map((option, index) => (
<div
key={`key${index}`}
onClick={(e) => handleOptionClick(e, option)}
className={Styles.selectOption}
style={{ fontSize: "14px" }}
>
{option.label}
</div>
))}
</div>
)}
</button>
);
}
export default LineHeight;