Отключение области в компоненте перетаскивания react hello-pangea/dnd

Вопрос или проблема

Я использую hello-pangea/dnd, чтобы создать область перетаскивания и сброса, и хочу иметь возможность перетаскивать и упорядочивать разделы в списке.

Однако мне не нужен просто ручка, я хочу иметь возможность перетаскивать их из любого места в div. Я могу это сделать, но, чтобы добавить что-то еще, я хотел бы иметь интерактивную область, посвященную чему-то другому в этом div, которая не затрагивалась бы при перетаскивании и сбросе.

Чтобы проиллюстрировать свою проблему, я хотел бы иметь возможность перетаскивать и сбрасывать, щелкнув и перетащив область collapsible-header
область перетаскивания

Но также исключить svg из области перетаскивания и сброса: если я взаимодействую с svg, я не хочу, чтобы весь раздел перемещался, я хочу иметь возможность перемещать синюю ручку на svg,
область, которую я хочу исключить из dnd

Я пробовал разные решения, но не добился успеха.

моя проблема, я не могу взаимодействовать с 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-значки, которые вы хотите исключить из зоны перетаскивания.

Вот шаги, которые нужно выполнить для достижения желаемого результата:

  1. Добавление обработчиков событий для SVG: Мы можем использовать обработчик событий onMouseDown для SVG-элементов и предотвращать распространение события, чтобы не активировать перетаскивание.

  2. Установка обработчиков в 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 не инициирует перетаскивание целого элемента.
  • Вы можете добавить возможность отключать перетаскивание для других элементов счетчика, если потребуется.
  • Убедитесь, что вы протестировали функциональность на всех платформах (браузерах), если это важно для вашего проекта.

Следуя этому подходу, вы сможете эффективно изолировать интерактивные элементы в вашем интерфейсе от функциональности перетаскивания, обеспечивая пользователям лучший опыт взаимодействия.

Оцените материал
Добавить комментарий

Капча загружается...