Вопрос или проблема
Я использую hello-pangea/dnd
, чтобы создать область перетаскивания и сброса, и хочу иметь возможность перетаскивать и упорядочивать разделы в списке.
Однако мне не нужен просто ручка, я хочу иметь возможность перетаскивать их из любого места в div. Я могу это сделать, но, чтобы добавить что-то еще, я хотел бы иметь интерактивную область, посвященную чему-то другому в этом div, которая не затрагивалась бы при перетаскивании и сбросе.
Чтобы проиллюстрировать свою проблему, я хотел бы иметь возможность перетаскивать и сбрасывать, щелкнув и перетащив область collapsible-header
Но также исключить svg из области перетаскивания и сброса: если я взаимодействую с svg, я не хочу, чтобы весь раздел перемещался, я хочу иметь возможность перемещать синюю ручку на svg,
Я пробовал разные решения, но не добился успеха.
Вот мой код:
import { DraggableProvidedDragHandleProps } from "@hello-pangea/dnd";
import CollapsibleSection from "../collapsibleSection";
export interface StatsSectionProps {
isExpanded: boolean;
onToggle: () => void;
dragHandleProps?: DraggableProvidedDragHandleProps;
}
export const StatsSection: React.FC<StatsSectionProps> = ({ isExpanded, onToggle, dragHandleProps }) => (
<CollapsibleSection header={<h3>Статистика</h3>} isExpanded={isExpanded} onToggle={onToggle} dragHandleProps={dragHandleProps}>
<div className="stats-info">
<p>Здесь будет отображаться статистическая информация.</p>
</div>
</CollapsibleSection>
);
export interface SpanSectionProps {
isExpanded: boolean;
onToggle: () => void;
timePeriod: {
totalDays: number;
monthsAndDays: string;
yearsMonthsDays: string;
xAxisLabels: string;
startDate: string;
endDate: string;
};
dragHandleProps?: DraggableProvidedDragHandleProps;
onDateChange: (startDate: string, endDate: string) => void;
}
export const SpanSection: React.FC<SpanSectionProps> = ({isExpanded, onToggle, timePeriod, dragHandleProps, onDateChange}) => {
const [localTimePeriod, setLocalTimePeriod] = useState(timePeriod);
useEffect(() => {
setLocalTimePeriod(timePeriod);
}, [timePeriod]);
const handleDateChange = (startDate: string, endDate: string) => {
setLocalTimePeriod(prev => ({ ...prev, startDate, endDate }));
onDateChange(startDate, endDate);
};
return (
<div className="span-section" style={{ width: '100%' }}>
<CollapsibleSection
header={
<TimelineAxis
fixedStartDate={timePeriod.startDate}
fixedEndDate={timePeriod.endDate}
initialStartDate={localTimePeriod.startDate}
initialEndDate={localTimePeriod.endDate}
onDateChange={handleDateChange}
/>
}
isExpanded={isExpanded}
onToggle={onToggle}
dragHandleProps={dragHandleProps}
>
<div className="time-period-info">
<p>Дата начала: {localTimePeriod.startDate}</p>
<p>Дата окончания: {localTimePeriod.endDate}</p>
<p>Всего дней: {localTimePeriod.totalDays}</p>
<p>Месяцы и дни: {localTimePeriod.monthsAndDays}</p>
<p>Годы, месяцы и дни: {localTimePeriod.yearsMonthsDays}</p>
<p>Подписи по оси X: {localTimePeriod.xAxisLabels}</p>
</div>
</CollapsibleSection>
</div>
);
};
import React, { useRef, useEffect, useState, useCallback } from 'react';
import * as d3 from 'd3';
interface TimelineAxisProps {
fixedStartDate: string;
fixedEndDate: string;
initialStartDate: string;
initialEndDate: string;
onDateChange: (startDate: string, endDate: string) => void;
}
const TimelineAxis: React.FC<TimelineAxisProps> = ({
fixedStartDate,
fixedEndDate,
initialStartDate,
initialEndDate,
onDateChange
}) => {
const [width, setWidth] = useState(0);
const svgRef = useRef<SVGSVGElement>(null);
const [startDate, setStartDate] = useState(initialStartDate);
const [endDate, setEndDate] = useState(initialEndDate);
const parseDate = d3.timeParse('%d/%m/%Y');
const formatDate = d3.timeFormat('%d/%m/%Y');
const updateWidth = useCallback(() => {
if (svgRef.current) {
const newWidth = svgRef.current.getBoundingClientRect().width;
setWidth(newWidth);
}
}, []);
useEffect(() => {
updateWidth();
window.addEventListener('resize', updateWidth);
return () => window.removeEventListener('resize', updateWidth);
}, [updateWidth]);
const fixedStart = parseDate(fixedStartDate);
const fixedEnd = parseDate(fixedEndDate);
const start = parseDate(startDate);
const end = parseDate(endDate);
if (!fixedStart || !fixedEnd || !start || !end) return null;
const years = d3.timeYear.range(fixedStart, d3.timeYear.offset(fixedEnd, 1));
const scale = d3.scaleTime()
.domain([fixedStart, fixedEnd])
.range([30, width - 30]);
const getVisibleYears = (years: Date[], scale: d3.ScaleTime<number, number>): Date[] => {
if (years.length <= 1) return years;
const totalSpace = Math.abs(scale(years[years.length - 1]) - scale(years[0]));
const averageSpace = totalSpace / (years.length - 1);
const minSpaceBetweenLabels = 50;
let step;
if (averageSpace >= minSpaceBetweenLabels) {
step = 1;
} else if (averageSpace * 2 >= minSpaceBetweenLabels) {
step = 2;
} else if (averageSpace * 3 >= minSpaceBetweenLabels) {
step = 3;
} else {
step = 4;
}
return years.filter((_, index) => index % step === 0);
};
const visibleYears = getVisibleYears(years, scale);
const handleDrag = (isStart: boolean) => (event: React.MouseEvent<SVGCircleElement>) => {
event.preventDefault();
event.stopPropagation();
const svg = svgRef.current;
if (!svg) return;
const startDrag = (e: MouseEvent) => {
e.preventDefault();
const mouseX = e.clientX - svg.getBoundingClientRect().left;
const date = scale.invert(mouseX);
const formattedDate = formatDate(date);
if (isStart) {
if (date >= fixedStart && date < end) {
setStartDate(formattedDate);
onDateChange(formattedDate, endDate);
}
} else {
if (date <= fixedEnd && date > start) {
setEndDate(formattedDate);
onDateChange(startDate, formattedDate);
}
}
};
const stopDrag = () => {
document.removeEventListener('mousemove', startDrag);
document.removeEventListener('mouseup', stopDrag);
};
document.addEventListener('mousemove', startDrag);
document.addEventListener('mouseup', stopDrag);
};
const handleSvgClick = (event: React.MouseEvent<SVGSVGElement>) => {
event.stopPropagation();
};
return (
<svg ref={svgRef} width="100%" height="60" onClick={handleSvgClick}>
<line x1="30" y1="30" x2={width - 30} y2="30" stroke="currentColor" />
{visibleYears.map((year, index) => (
<g key={index} transform={`translate(${scale(year)}, 0)`}>
<line x1="0" y1="25" x2="0" y2="35" stroke="currentColor" />
<text x="0" y="20" textAnchor="middle" fontSize="12">{year.getFullYear()}</text>
</g>
))}
<circle cx={scale(start)} cy="30" r="8" fill="var(--primary-color, blue)" cursor="ew-resize" onMouseDown={handleDrag(true)} />
<circle cx={scale(end)} cy="30" r="8" fill="var(--primary-color, blue)" cursor="ew-resize" onMouseDown={handleDrag(false)} />
<text x={scale(start)} y="50" textAnchor="middle" fontSize="12">{startDate}</text>
<text x={scale(end)} y="50" textAnchor="middle" fontSize="12">{endDate}</text>
</svg>
);
};
export default TimelineAxis;
interface SectionsProps {
variables: Omit<VariablesSectionProps, 'dragHandleProps'>;
span: Omit<SpanSectionProps, 'dragHandleProps'>;
predictions: Omit<PredictionsSectionProps, 'dragHandleProps'>;
stats: Omit<StatsSectionProps, 'dragHandleProps'>;
}
type SectionKey = keyof SectionsProps;
const DraggableSections: React.FC<{ sectionsProps: SectionsProps }> = ({ sectionsProps }) => {
const [sections, setSections] = useState<Array<{ id: SectionKey; component: React.ComponentType<any> }>>([
{ id: 'span', component: SpanSection },
{ id: 'stats', component: StatsSection },
]);
const onDragEnd = (result: any) => {
if (!result.destination) return;
const items = Array.from(sections);
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
setSections(items);
};
return (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="sections">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{sections.map((section, index) => {
const SectionComponent = section.component;
return (
<Draggable key={section.id} draggableId={section.id} index={index}>
{(provided, snapshot) => (
<div ref={provided.innerRef} {...provided.draggableProps} className={`draggable-section ${snapshot.isDragging ? 'is-dragging' : ''}`}>
<SectionComponent {...sectionsProps[section.id]} dragHandleProps={provided.dragHandleProps} />
</div>
)}
</Draggable>
);
})}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
};
export default DraggableSections;
Я не смог найти рабочее решение. У вас есть идеи/предложения о том, как подойти к решению этой проблемы?
Ответ или решение
Для решения задачи отключения определённой области в компоненте перетаскивания с использованием библиотеки hello-pangea/dnd
, можно воспользоваться следующим подходом. Мы будем отслеживать события мыши и предотвращать перетаскивание, если пользователь взаимодействует со специфическими элементами интерфейса, такими как SVG-значки, которые вы хотите исключить из зоны перетаскивания.
Вот шаги, которые нужно выполнить для достижения желаемого результата:
-
Добавление обработчиков событий для SVG: Мы можем использовать обработчик событий
onMouseDown
для SVG-элементов и предотвращать распространение события, чтобы не активировать перетаскивание. -
Установка обработчиков в
Draggable
: Обрабатывать событиеonMouseDown
непосредственно вDraggable
, чтобы убедиться, что оно не захватывается, когда происходит взаимодействие с SVG.
Вот обновлённый код вашего компонента TimelineAxis
, который включает в себя изменения:
const handleSvgClick = (event: React.MouseEvent<SVGSVGElement>) => {
event.stopPropagation(); // предотвращаем распространение события
};
const handleDragStart = (event: React.MouseEvent<SVGCircleElement>) => {
event.stopPropagation(); // предотвращаем перетаскивание при взаимодействии с SVG
};
return (
<svg ref={svgRef} width="100%" height="60" onClick={handleSvgClick}>
<line x1="30" y1="30" x2={width - 30} y2="30" stroke="currentColor" />
{visibleYears.map((year, index) => (
<g key={index} transform={`translate(${scale(year)}, 0)`}>
<line x1="0" y1="25" x2="0" y2="35" stroke="currentColor" />
<text x="0" y="20" textAnchor="middle" fontSize="12">{year.getFullYear()}</text>
</g>
))}
<circle cx={scale(start)} cy="30" r="8" fill="var(--primary-color, blue)" cursor="ew-resize"
onMouseDown={(e) => { handleDrag(true)(e); handleDragStart(e); }} />
<circle cx={scale(end)} cy="30" r="8" fill="var(--primary-color, blue)" cursor="ew-resize"
onMouseDown={(e) => { handleDrag(false)(e); handleDragStart(e); }} />
<text x={scale(start)} y="50" textAnchor="middle" fontSize="12">{startDate}</text>
<text x={scale(end)} y="50" textAnchor="middle" fontSize="12">{endDate}</text>
</svg>
);
Объяснение изменений:
-
event.stopPropagation()
: Вызов этого метода вhandleSvgClick
иhandleDragStart
предотвращает активирование обработчиков событий на родительских элементах, что позволяет избежать срабатывания перетаскивания при взаимодействии с SVG-элементами. -
Передача
onMouseDown
с вызовомhandleDrag
: Это необходимо, чтобы правильно обрабатывать начальные действия перетаскивания для провода (SVG-круга), при этом восстанавливается реакция на перетаскивание, если колёсико мыши не используется для взаимодействия с SVG.
Дополнительные советы:
- Протестируйте изменения, чтобы убедиться, что перетаскивание работает корректно в нужных зонах, и взаимодействие с SVG не инициирует перетаскивание целого элемента.
- Вы можете добавить возможность отключать перетаскивание для других элементов счетчика, если потребуется.
- Убедитесь, что вы протестировали функциональность на всех платформах (браузерах), если это важно для вашего проекта.
Следуя этому подходу, вы сможете эффективно изолировать интерактивные элементы в вашем интерфейсе от функциональности перетаскивания, обеспечивая пользователям лучший опыт взаимодействия.