Início Tecnologia Fazendo um menu arrastável no React usando o dndkit

Fazendo um menu arrastável no React usando o dndkit

13
0

Recentemente, me deparei com um problema em que preciso fazer blocos em movimento que se assemelhem a menus. Eu precisava da funcionalidade de arrastar botões em qualquer direção e lado a lado.

Depois de pesquisar as bibliotecas, escolhi o DND Kit. Lá, somos recebidos por documentação legal e muitos exemplos. A coisa mais importante que precisamos são áreas em que podemos arrastar e soltar itens e personalizar a funcionalidade de maneira muito eficaz. O principal aqui é usar ganchos: quando nos movemos, este é o usadoRaggable gancho e a área onde inserimos UsadoRoppable. Bem, e todos os tipos de adições na forma de animações e classificação seguidas.

UI e preparação

Baixe as bibliotecas

yarn add @dnd-kit/core @dnd-kit/sortable

Não estamos envolvendo todo o aplicativo, mas um componente específico em que o DND ocorrerá.

Nós colocamos o sensores no contexto. Vamos usá -los para determinar como lidar com blocos de arrasto com toque ou mouse.

Depois de experimentar algoritmos de colisão, escolhi collisionDetection=pointerWithin. O melhor para o nosso caso. O algoritmo é guiado pelas coordenadas do clique e pelos blocos que se cruzam com ele.

Vamos criar o estado do bloco ativo que estamos arrastando. Vamos escrever seu ID no HandledRagstart manipulador. No final do arrasto, ligaremos manipulou -se e redefinir o estado. E o manipulador de manipulação será chamado quando nosso bloco estiver acima da possível área de inserção.

import {
  pointerWithin,
  useSensors,
} from "@dnd-kit/core";

const sensorSettings = {
  distance: 2,
};

export default function DndRoot() {
  const [activeDndItemId, setActiveDndItemId] = useState(null);
  const sensors = useSensors(
    useSensor(MouseSensor, {
      activationConstraint: sensorSettings,
    }),
    useSensor(PointerSensor, {
      activationConstraint: sensorSettings,
    }),
  );
  
  const handleDragStart = ({active}: DragStartEvent) => {
    setActiveDndItemId(active.id as number);
  };
  
  const handleDragEnd = ({over}: DragOverEvent) => {
    setActiveDndItemId(null);
  };
  
  const handleDragOver = ({active, over}: DragOverEvent) => {
    // Обработаем позже
  } 
  
  return (
    
       {/* Cюда будем добавлять элементы */}
     
  );
} 

Em seguida, sugiro adicionar o estado de nossos botões ao componente Dndroot ou superior. O campo do pedido será responsável pela ordem dos botões dentro da linha. ROWNUMER para a localização da própria linha. Começamos a indexação para ambos os campos de 1. Dessa forma, podemos armazenar facilmente nosso estado de botão no banco de dados. Você também pode adicionar imediatamente o estado inicial para os botões (na parte inferior, haverá um link para o CodESandBox, você pode levá -lo a partir daí).

interface IButton {
  id: number;
  text: string;
  color: string;
  order: number;
  rowNumber: number;
}

const [buttons, setButtons] = useState(initialButtons);

Vá para o componente do botão. Uma matriz desses botões será renderizada em cada linha. Aqui, quando isdraggingescondemos isso para que mais tarde possamos fazer uma bela animação usando um ponto de referência especial e parecer visualmente o mesmo botão em outro lugar. A transformação e a transição do CSS precisam garantir o local correto ao classificar em uma linha. Os IDs desempenham um grande papel aqui e nas linhas, então devem ser únicos. usaRortable monitorará a referência e os eventos do usuário, por exemplo, pressionando e segurando um botão nesse bloco. Nós lançaremos os adereços necessários a partir daí para o botão.

export interface IDndBtnProps {
  btn: IButton;
  rowLength: number;
}

export const DndBtn = ({ btn, rowLength }: IDndBtnProps) => {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({
    id: btn.id,
  });
  const btnStyle = {
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: isDragging ? 0 : 1,
  };

  return (
    
  );
};

Componente da linha. Adicionaremos classificação e linhas à matriz de fora em Dndroot, e aqui mapeamos a matriz de botões desta linha da row: iButton[].

SortableContext Aqui desempenha o papel óbvio de classificar de acordo com uma determinada estratégia, neste caso, ela classifica apenas horizontalmente. Ele trocará temporariamente os blocos na linha. Mas vamos mudar nossa propriedade de pedido apenas em manipulou -sequando o usuário já decidiu a escolha e libera o clique.

interface IDndRowProps {
  row: IButton[];
  rowId: string;
}

export const DndRow = ({ row, rowId }: IDndRowProps) => {
  const { setNodeRef } = useDroppable({
    id: rowId,
  });
  return (
    
      

{row.map((btn) => ( ))}

); };

Aqui passamos por todos os nossos botões e os organizamos em linhas com base no ROWNUME propriedade. Também classificamos em uma linha pela propriedade Order. Agora temos toda a interface do usuário pronta e os ganchos podem lidar com o DND.

export default function DndRoot() {
 
  const getButtons = () => {
    if (!buttons || !buttons.length) return null;
    let res: IButton[][] = [];
    buttons
      ?.sort((a, b) => a.order - b.order)
      .forEach((btn) => {
        if (!res[btn.rowNumber]) res[btn.rowNumber] = [];
        res[btn.rowNumber] = [...res[btn.rowNumber], btn];
      });
    res = res.filter((el) => el);
    return res.map((row, i) => );
  };
  
  return (
    
       {getButtons()}
     
  );
}

DND lógica

Vamos para nossos 3 manipuladores da raiz. Já armazenamos IDs do bloco arrastado em Activednditemid. O gancho UsaRortable no próprio botão rastreia os eventos do usuário neste bloco, então no parâmetro recebido Active.id handleDragStart = ({active}: DragStartEvent) terá nosso ID do botão.

Em handleDragOver = ({active, over}: DragOverEvent)podemos verificar onde nosso bloco móvel (over) está localizado atualmente e qual bloco está ativo. Como você se lembra, usamos um ID de string especial (`Row $ {i}`) na linha, para que não o processemos aqui. Nós comparamos que o over O ID do bloco difere do ID do bloco ativo e eles existem. Então vamos mudar o ROWNUME campo em nosso botão.

Em handleDragEnd = ({ over }: DragOverEvent)mudamos a ordem. Nossos ganchos de classificação alteram visualmente a posição horizontal dos botões seguidos. Mas quando liberarmos o bloco, o pedido não mudará. Portanto, no final, corrigimos isso no estado.

E quem recalculará o número e a ordem da linha se quisermos adicionar novas linhas ou mover todos os botões de uma linha para outra linha ou mover o último botão antes do primeiro na mesma linha?

Após cada mudança de linha, precisamos recalcular sua ordem no estado. Para evitar tais discrepâncias:

[
  { ... id: 1, rowNumber: 1 },
  { ... id: 2, rowNumber: 5 }
]

O ROWNUMBER mudou com o GAP depois de arrastar os botões das linhas originais para outras pessoas e desaparecer as primeiras. O mesmo deve ser feito para ordem. Por uma questão de experimento, fizemos um aviso no ChatGPT e geramos nossos algoritmos:

const updateButtonsRowOrder = (
  array: IButton[],
  id: number,
  newOrder: number
) => {
  const item = array.find((el) => el.id === id);
  if (!item) return array; // If the id is not found, we return the array unchanged.
  // Removing an element from the array and sorting the remaining elements by order
  const filteredArray = array
    .filter((el) => el.id !== id)
    .sort((a, b) => a.order - b.order);

  // Inserting the element with the new order in the desired position
  filteredArray.splice(newOrder - 1, 0, { ...item, order: newOrder });

  // We just inserted the necessary element into the array and so the array is sorted, we will replace all the order with index+1.
  return filteredArray.map((el, index) => ({ ...el, order: index + 1 }));
};

const updateButtonsRowNumber = (
  array: IButton[],
  id: number,
  newRowNumber: number
) => {
  const item = array.find((el) => el.id === id);
  if (!item) return array; // If the id is not found, we return the array unchanged.
  // Updating the row Number of the specified element
  item.rowNumber = newRowNumber;

  // Sorting the array by RowNumber
  const sortedArray = array.sort((a, b) => a.rowNumber - b.rowNumber);

  // Recalculating the RowNumber so that there are no gaps
  const uniqueRowNumbers = [
    ...new Set(sortedArray.map((el) => el.rowNumber)),
  ].sort((a, b) => a - b);

  const mapping = new Map(
    uniqueRowNumbers.map((num, index) => [num, index + 1])
  );

  // We find the RowNumber in mapping and take the index+1 from mapping
  sortedArray.forEach((el) => {
    el.rowNumber = mapping.get(el.rowNumber) ?? el.rowNumber;
  });

  return sortedArray;
};

Agora inserimos os algoritmos nos manipuladores:

const handleDragEnd = ({ over }: DragOverEvent) => {
    if (!activeDndItemId) return;
    const overIndex = over?.data.current?.sortable.index || 0;
    setButtons((prev) => {
      const res = prev.map((btn) => {
        if (btn.id === activeDndItemId) {
          return {
            ...btn,
            order: overIndex + 1,
          };
        }
        return btn;
      });
      return updateButtonsRowOrder(res, activeDndItemId, overIndex + 1);
    });
    setActiveDndItemId(null);
  };

  const handleDragOver = ({ active, over }: DragOverEvent) => {
    const activeBtnId = active.id;
    const overBtnId = over?.id;
    const activeRowNumber = buttons?.find(
      (btn) => btn.id === activeBtnId
    )?.rowNumber;
    const overRowNumber = buttons?.find(
      (btn) => btn.id === overBtnId
    )?.rowNumber;

    if (
      !activeDndItemId ||
      !activeRowNumber ||
      !overRowNumber ||
      activeRowNumber === overRowNumber
    ) {
      return;
    }

    setButtons((prev) => {
      const res = prev.map((btn) => {
        if (btn.id === activeDndItemId) {
          return {
            ...btn,
            rowNumber: overRowNumber,
          };
        }
        return btn;
      });
      return updateButtonsRowNumber(res, activeDndItemId, overRowNumber);
    });
  };

Animação no DND

No final, fazemos uma bela animação ao transferir. Na verdade, inseriremos uma cópia do nosso botão em uma renderização especial da biblioteca e ela a renderizará. Essa abordagem é recomendada para casos complexos e animações suaves.


  const overlayItem = useMemo(() => {
    return buttons?.find((btn) => btn.id === activeDndItemId);
  }, [activeDndItemId, buttons]);

  
    {getButtons()}
    
      {overlayItem ? (
        
      ) : null}
    
  

O que vem a seguir?

Acabou sendo muito código, desde que não adicionemos complicações: corte o texto, excedendo o tamanho do botão, ajustando o máximo de botões em uma linha, etc.

Você também pode adicionar uma inserção acima ou abaixo de qualquer linha, usando nosso ID especial row${i} ou apenas um botão regular na parte inferior (como no vídeo) com a adição de uma linha e um novo bloco. Toda a funcionalidade, além da base, pode ser aprimorada usando nossos 3 manipuladores na raiz.

Eu também forneço um

para a caixa de areia com o código completo deste artigo.

Boa sorte com DND!

fonte