diff --git a/src/plugins/textReplace/index.tsx b/src/plugins/textReplace/index.tsx index b26b24acf..7658e5e5d 100644 --- a/src/plugins/textReplace/index.tsx +++ b/src/plugins/textReplace/index.tsx @@ -178,9 +178,12 @@ function normalizeRule(rule: Rule) { rule.id ??= crypto.randomUUID(); } +type DropIndicator = { index: number; position: "before" | "after"; }; + function TextReplace({ title, description, rulesArray, isRegex = false }: TextReplaceProps) { const [searchQuery, setSearchQuery] = useState(""); const [draggedIndex, setDraggedIndex] = useState(null); + const [dropIndicator, setDropIndicator] = useState(null); function onClickRemove(index: number) { rulesArray.splice(index, 1); @@ -209,12 +212,35 @@ function TextReplace({ title, description, rulesArray, isRegex = false }: TextRe return acc; }, []); - const handleDrop = (index: number) => (e: React.DragEvent) => { + const handleDragOver = (index: number) => (e: React.DragEvent) => { e.preventDefault(); - if (draggedIndex === null || draggedIndex === index || searchQuery) return; - const draggedRule = rulesArray.splice(draggedIndex, 1)[0]; - rulesArray.splice(index, 0, draggedRule); + if (draggedIndex === null || searchQuery) return; + + const rect = e.currentTarget.getBoundingClientRect(); + const position = e.clientY - rect.top < rect.height / 2 ? "before" : "after"; + setDropIndicator(prev => + prev?.index === index && prev.position === position ? prev : { index, position } + ); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + if (draggedIndex === null || !dropIndicator || searchQuery) { + setDraggedIndex(null); + setDropIndicator(null); + return; + } + + let targetIndex = dropIndicator.index + (dropIndicator.position === "after" ? 1 : 0); + if (targetIndex > draggedIndex) targetIndex -= 1; + + if (targetIndex !== draggedIndex) { + const draggedRule = rulesArray.splice(draggedIndex, 1)[0]; + rulesArray.splice(targetIndex, 0, draggedRule); + } + setDraggedIndex(null); + setDropIndicator(null); }; return ( @@ -222,11 +248,13 @@ function TextReplace({ title, description, rulesArray, isRegex = false }: TextRe
{title} {description} - +
+ +
{!filteredRules.length && searchQuery && ( @@ -235,10 +263,16 @@ function TextReplace({ title, description, rulesArray, isRegex = false }: TextRe {filteredRules.map(({ rule, index }) =>
e.preventDefault()} - onDrop={handleDrop(index)} - style={{ opacity: draggedIndex === index ? 0.5 : 1 }} + onDragOver={handleDragOver(index)} + onDrop={handleDrop} + style={{ opacity: draggedIndex === index ? 0.5 : 1, position: "relative" }} > + {dropIndicator?.index === index && dropIndicator.position === "before" && ( +
+ )} + {dropIndicator?.index === index && dropIndicator.position === "after" && ( +
+ )} ( <> @@ -293,7 +327,10 @@ function TextReplace({ title, description, rulesArray, isRegex = false }: TextRe setDraggedIndex(index); e.dataTransfer.setData("text/plain", index.toString()); }} - onDragEnd={() => setDraggedIndex(null)} + onDragEnd={() => { + setDraggedIndex(null); + setDropIndicator(null); + }} style={{ cursor: searchQuery ? "default" : "grab", flex: 1 }} > diff --git a/src/plugins/textReplace/styles.css b/src/plugins/textReplace/styles.css index 51a65fe1a..b58f55137 100644 --- a/src/plugins/textReplace/styles.css +++ b/src/plugins/textReplace/styles.css @@ -8,4 +8,19 @@ .vc-textReplace-delete-button { width: 100%; margin-block: 0.75em 0.5em +} + +.vc-textReplace-drop-indicator { + position: absolute; + left: 0; + right: 0; + height: 2px; + background: var(--brand-experiment, #5865f2); + border-radius: 1px; + pointer-events: none; + z-index: 1; +} + +.vc-textReplace-search-input { + margin-top: 8px; } \ No newline at end of file