React-dnd: Conectar monitor.getClientOffset a um DropTarget não atualiza props

Criado em 3 jun. 2015  ·  22Comentários  ·  Fonte: react-dnd/react-dnd

Criei um DropTarget e conectei getClientOffset da seguinte maneira:

targetCollect = function (connect, monitor) {
  return {
    connectDropTarget: connect.dropTarget(),
    isOver: monitor.isOver(),
    clientOffset: monitor.getClientOffset()
  }
}

No entanto, ao mover o arrasto dentro do componente, o alvo não recebe adereços atualizados com cada movimento do mouse (apenas ao entrar e sair). O foco da especificação de destino _é_ chamado repetidamente. Existe uma razão para que o prop clientOffset não seja injetado cada vez que é alterado?

bug wontfix

Comentários muito úteis

+1. Correndo para isso e perdendo muito tempo tentando entender por que getClientOffset () funciona bem em canDrop () - que é chamado repetidamente ... - mas não há como encaminhar para o meu controle 'onde' sobre o item que estamos. Preciso ser capaz de inserir acima e abaixo de um treenó, não apenas "nele". canDrop () não tem como encontrar que me permita dizer ao meu componente qual dos muitos "tipos" de drops o nó possui, para que meu controle possa ser exibido novamente de acordo. Alterar o prop em canDrop resultou em um erro. A assinatura do onMouseMove não funciona durante as operações de arrastar e soltar. Isso tem sido uma grande perda de tempo até agora ... qualquer ajuda seria apreciada.

@ibash , você se importaria de postar uma ideia do que você fez para resolver seu problema?

Todos 22 comentários

Oh, bom ponto. Isso mesmo, não antecipei esse uso. Atualmente, a única maneira de optar por rastrear a compensação do cliente é usar DragLayer . Caso contrário, por motivos de desempenho, ele apenas atualiza os adereços se algo relacionado ao _drop próprio alvo_ for alterado.

Eu diria que é um problema de API e pode haver várias soluções:

  • Proibir acessar getClientOffset() e métodos semelhantes de dentro da função collect e dizer para usar DragLayer lugar (fácil, mas estúpido);
  • Descobrir automaticamente que o usuário deseja rastrear o deslocamento e assinar as alterações de deslocamento, se for esse o caso (mais difícil, mas mais consistente).

Acho que aceitaria um RP fazendo a segunda opção. Você pode verificar a implementação de DragLayer para ver como ela se inscreve nas alterações de deslocamento.

Eu olhei para a segunda opção, mas é complicado porque collect é uma função, então não é possível / fácil descobrir quais funções de monitor podem ser chamadas.

Meu caso de uso é arrastar widgets que são encaixados no local, possivelmente deslocando outros widgets de uma forma relativamente complexa (pense em arrastar ícones de programa em um Android). É realmente o destino que precisa se reorganizar dependendo da localização exata do foco, enquanto o arrasto em si está perfeitamente ok como um instantâneo HTML5. Posso fazer isso com o foco da especificação de destino, atualizando o estado do componente de destino. Então, vou deixar isso por agora e resolver isso. Obrigado por uma biblioteca incrível; Eu tenho algo decente trabalhando em poucas horas de trabalho.

Vamos manter isso aberto por enquanto. É o mesmo que # 172: não antecipei pessoas usando deltas dentro de canDrop ou a função de coleta. Não vou consertar isso agora, mas posso liberar depois de um mês ou mais.

Eu também tive esse problema. O que você acha de permitir que os clientes passem um sinalizador para a especificação do DropTarget para se inscrever para compensar as alterações? É mais um "pegadinho" para o usuário, mas é mais simples do que inscrever / cancelar a inscrição manualmente em compensações.

Você não poderia usar RxJS-DOM para observar o evento dragover no elemento e atualizar o estado com as coordenadas do mouse? Solução Hacky, talvez.

  componentDidMount() {
    this._events.dragOver = DOM.fromEvent(findDOMNode(this.refs.grid), 'dragover')
                            .throttle(200)
                            .subscribe(this.getMouseCoords)
  }

Acabei pegando o monitor e me inscrevendo para compensar mudanças como o DragLayer. Também acabei acessando o registro interno em alguns lugares. Não acho que o que eu quero fazer seja _muito_ incomum, então, IMO, isso poderia ser resolvido por simples mudanças de API.

Algo resultou disso? Gostaria de monitorar o deslocamento do cliente em meu DropTarget em vez de na camada de arrastar personalizada, porque sinto que é mais fácil alterar a estrutura do DropTarget quando posso acessar o local de foco do DropTarget em vez da camada de arrastar personalizada.

+1 Eu gostaria de fazer exatamente o que @jchristman está tentando fazer.

+1. Correndo para isso e perdendo muito tempo tentando entender por que getClientOffset () funciona bem em canDrop () - que é chamado repetidamente ... - mas não há como encaminhar para o meu controle 'onde' sobre o item que estamos. Preciso ser capaz de inserir acima e abaixo de um treenó, não apenas "nele". canDrop () não tem como encontrar que me permita dizer ao meu componente qual dos muitos "tipos" de drops o nó possui, para que meu controle possa ser exibido novamente de acordo. Alterar o prop em canDrop resultou em um erro. A assinatura do onMouseMove não funciona durante as operações de arrastar e soltar. Isso tem sido uma grande perda de tempo até agora ... qualquer ajuda seria apreciada.

@ibash , você se importaria de postar uma ideia do que você fez para resolver seu problema?

Mudar para o hover finalmente funcionou para mim.

`passar: (props: MyProps, monitor: DropTargetMonitor, componente: ReportItemRow) => {
if (componente! = nulo) {
const clientOffset = monitor.getClientOffset ();

        // Is the dragging node dragged above/below us as opposed to "on" us?

        const elementRect = document.elementFromPoint(clientOffset.x, clientOffset.y).getBoundingClientRect();

        const percentageY = (clientOffset.y - elementRect.top) / elementRect.height;

        // Divide the box up into .25 .5 .25
        const insertDragAndDropMagnetPercent = .25;

        if (insertDragAndDropMagnetPercent >= percentageY) {
            component.setState({ dropTarget: "above" });
        }
        else if (1 - insertDragAndDropMagnetPercent >= percentageY) {
            component.setState({ dropTarget: "inside" });
        }
        else {
            component.setState({ dropTarget: "below" });
        }
    }
},`

@ noah79 , enquanto espera que algo aconteça, você pode resolver seu problema criando uma série de Alvos de queda e colocando-os sob seus tréenodos. Algo assim:

         _____
         _____| <-- drop div1 (above treenode1/below top of list)
treenode1_____| <-- drop div2 (on treenode1)
         _____| <-- drop div3 (below treenode1/above treenode2)
treenode2_____| <-- drop div4 (on treenode 2)
         _____| <-- drop div5 (below treenode2/above bottom of list)

Você acaba com 2n + 1 alvos de descarte, onde n é o número de elementos em sua lista. Então, com base em qual dos divs está sendo passado, você pode alterar a aparência da lista de árvore. Fiz algo muito semelhante a isso para contornar a impossibilidade de acessar getClientOffset () por enquanto, e não notei uma queda no desempenho. A altura de cada drop div deve ser 1/2 da altura da linha treenode1 e a primeira drop div deve estar localizada 1/4 da altura da linha treenode1 acima da primeira linha. Ou seja, seu CSS deve ser assim:

.dropdiv-container {
    position: absolute;
    top: -0.25em; /* If the top of this container is aligned with the top of the treenode list initially */
}
.dropdiv {
    height: 0.5em; /* If the height of treenode1 line is 1em with no padding */
}
<div className='dropdiv-container'>
    { renderDropTargets(2 * listLength + 1) }
</div>

Isso faz sentido?

Aqui está uma ideia de como resolvi isso. O truque é apenas alcançar o
internals react-dnd e use o monitor global diretamente. Dê uma olhada no
Fonte DragDropMonitor.js para ter uma ideia de quais métodos estão disponíveis.
ref: https://github.com/gaearon/dnd-core/blob/master/src/DragDropMonitor.js

Você terá que desculpar o coffeescript :)

  # in the component being dragged, get access to the dragDropManager by adding
  # it to the contextTypes
  #
  # dragDropManager is an instance of this:
  # https://github.com/gaearon/dnd-core/blob/master/src/DragDropManager.js

  <strong i="11">@contextTypes</strong>: {
    dragDropManager: React.PropTypes.object
  }

  # because we can receive events after the component is unmounted, we need to
  # keep track of whether the component is mounted manually.
  #
  # <strong i="12">@_monitor</strong> is what lets us access all the nice internal state - it is an instance of this:
  # https://github.com/gaearon/dnd-core/blob/master/src/DragDropMonitor.js

  componentWillMount: () =>
    <strong i="13">@_isMounted</strong> = true
    <strong i="14">@_monitor</strong> = @context.dragDropManager.getMonitor()

    <strong i="15">@_unsubscribeToStateChange</strong> = @_monitor.subscribeToStateChange(@_onStateChange)
    <strong i="16">@_unsubscribeToOffsetChange</strong> = @_monitor.subscribeToOffsetChange(@_onOffsetChange)

  componentWillUnmount: () =>
    <strong i="17">@_isMounted</strong> = false
    <strong i="18">@_monitor</strong> = null

    @_unsubscribeToStateChange()
    @_unsubscribeToOffsetChange()

  # we can access dragging / dropping state by accessing the monitor 

  _onStateChange: () =>
    return unless <strong i="19">@_isMounted</strong>

    # When we stop dragging reset the counter state and hide all cursors.
    if <strong i="20">@_previousIsDragging</strong> && !@_monitor.isDragging()
      console.log('no longer dragging')
    <strong i="21">@_previousIsDragging</strong> = @_monitor.isDragging()

  _onOffsetChange: () =>
    return unless <strong i="22">@_isMounted</strong>

    # mouse is the x/y coordinates
    mouse = @_monitor.getClientOffset()

    # item is the drag item
    item = @_monitor.getItem()

    # if you want to check if a dragged item is over a target, you need the
    # targetId -- in the DropTarget wrapper you can pass it in like:
    #
    #   (connect, monitor) ->
    #     {
    #       targetId: monitor.targetId,
    #       connectDropTarget: connect.dropTarget()
    #     }
    #
    # and then you can use it like below

    @_monitor.isOverTarget(@props.targetId))

Isso faz sentido? Se não posso adicionar mais detalhes

Eu também estava passando por isso hoje e perdi algumas horas. Que tal deixar uma nota sobre isso na documentação em DropTarget -> A função de coleta? Isso, pelo menos, pouparia alguns outros de frustrações.

Btw. outro truque realmente feio em torno desse problema pode ser enviar as coordenadas _como estado_ de dropTarget.hover() :

const dropTarget = {
    hover(props, monitor, component) {
        // HACK! Since there is an open bug in react-dnd, making it impossible
        // to get the current client offset through the collect function as the
        // user moves the mouse, we do this awful hack and set the state (!!)
        // on the component from here outside the component.
        component.setState({
            currentDragOffset: monitor.getClientOffset(),
        });
    },
    drop() { /* ... */ },
};

Claro que existem várias maneiras mais corretas de fazer isso que evitariam o abuso de setState dessa forma. Mas pelo menos esse pequeno hack é muito condensado e pode funcionar até que o problema seja resolvido. Ele permite que você contorne o bug sem alterar outros componentes e / ou arquivos e sem depender de componentes internos da biblioteca.

Edit: isso é basicamente o mesmo que noah79, só não li seu código antes.

Não há realmente nenhuma maneira de obter a posição do mouse ao soltar? Meu componente de destino não precisa saber sobre isso, então setState não é uma opção para mim. Eu só preciso das coordenadas para acionar uma ação de redux.

Por favor, ignore minha última mensagem. Vejo agora que drop e endDrag são duas partes diferentes de funcionalidade, e obter o posicionamento do cursor em drop funciona conforme o esperado. :)

Em meus testes, o react-dnd está consumindo os eventos de movimento do mouse assim que um item é arrastado sobre o alvo. Se fosse possível para o html-backend não consumir este evento, o componente de destino poderia apenas colocar um ouvinte mousemove normal em seu dom condicionalmente quando a propriedade isOver for definida como true. Posteriormente, esse ouvinte pode pegar a posição do mouse (da maneira normal) quando algo está sendo arrastado sobre ele. Eu fiz isso funcionar temporariamente usando setState do método drag (). A reação está dando avisos sobre a configuração do estado no meio de uma transição de renderização

Isso também me fez tropeçar. +1 para uma solução API ou pelo menos algo no FAQ.

Existe algum plano para apoiar isso?
Eu tenho uma ideia para uma solução alternativa sem hack: faça um wrapper para DropTarget que também envolve hover para acionar outra chamada para a função de coleta e uma re-renderização se a função de coleta retornar novos valores .

1 para este problema. O caso de uso que tenho é vários alvos de queda que estão próximos uns dos outros. Parece estranho que, ao pairar, o alvo de queda adequado é realçado, mas, na verdade, a queda cairá em um alvo mais baixo dependendo da direção de onde você pairou.

Me deparei com este problema pesquisando por que os offsets não estão sendo atualizados, parece que ainda precisa de uma solução alternativa.
Eu peguei a ideia de @ jedwards1211 e aqui está o que eu tive. O HOC tem basicamente a mesma interface que o vanilla DropTarget , mas aceita collectRapidly em suas opções, que é uma lista de adereços a serem consultados por meio da função de coleta + passada para baixo em cada evento de foco. Passar todos os adereços indiscriminadamente da função de coleta causou alguma estranheza e não podemos passar connect para o colecionador, portanto, há um alerta contra consultar connectDropTarget "rapidamente".

Portanto, em vez de DropTarget(types, target, collect) você usaria RapidFireDropTarget(types, target, collect, {collectRapidly: ['offset']}) , onde offset é presumivelmente algo recebendo seu valor da família de funções monitor.getOffset .

import React from 'react';
import {DropTarget} from 'react-dnd';
import pick from 'lodash/pick';

function RapidFireDropTarget(types, spec, collect, options={}) {
  let collectProps = {};
  const prevHover = spec.hover;
  const {collectRapidly = []} = options;

  const dummyConnect = {
    dropTarget: () => () => {
      throw new Error('Rapidly collected props cannot include results from `connect`');
    }
  };

  const WrappedComponent = Component => {
    return class extends React.Component {
      render() {
        return <Component {...this.props} {...collectProps} />;
      }
    };
  };

  spec.hover = (props, monitor, component) => {
    const allCollectProps = collect(dummyConnect, monitor);

    collectProps = pick(allCollectProps, collectRapidly);
    component.forceUpdate();

    if (prevHover instanceof Function) {
      prevHover(props, monitor, component);
    }
  }

  return (Component) => {
    return DropTarget(types, spec, collect, options)(WrappedComponent(Component));
  };
}

export default RapidFireDropTarget;

Este problema foi marcado automaticamente como obsoleto porque não teve atividades recentes. Ele será fechado se nenhuma outra atividade ocorrer. Obrigado por suas contribuições.

Esta página foi útil?
0 / 5 - 0 avaliações