Как мне отобразить элемент-заполнитель в текущей точке вставки во время операции перетаскивания?

#reactjs

#reactjs

Вопрос:

У меня есть список, который я могу отсортировать с помощью перетаскивания с помощью react, и он отлично работает. Способ, которым это работает, — onDragEnter, элементы заменяются. Однако что я хочу сделать, так это показать элемент-заполнитель, как только перетаскиваемый элемент будет наведен на доступное пространство. Таким образом, окончательное размещение будет происходить в onDragEnd. У меня есть две функции, которые обрабатывают перетаскивание:

   const handleDragStart = (index) => {
    draggingItem.current = index;
  };

  const handleDragEnter = (index) => {
    if (draggingWidget.current !== null) return;

    dragOverItem.current = index;

    const listCopy = [...rows];
    const draggingItemContent = listCopy[draggingItem.current];

    listCopy.splice(draggingItem.current, 1);
    listCopy.splice(dragOverItem.current, 0, draggingItemContent);

    if (draggingItem.current === currentRowIndex) {
      setCurrentRowIndex(dragOverItem.current);
    }

    draggingItem.current = dragOverItem.current;
    dragOverItem.current = null;

    setRows(listCopy);
  };
 

и в шаблоне react jsx у меня есть это:

       {rows.map((row, index) => (
        <div
          key={index}
          draggable
          onDragStart={() => handleDragStart(index)}
          onDragEnter={() => handleDragEnter(index)}
          onDragOver={(e) => e.preventDefault()}
          onDragEnd={handleDragEndRow}
        >
...
</div>
 

Может ли кто-нибудь дать какие-либо советы относительно того, как я мог бы решить эту проблему?

Ответ №1:

Чтобы отобразить заполнитель, указывающий, куда вы собираетесь поместить перетаскиваемый элемент, вам необходимо вычислить точку вставки в соответствии с текущей позицией перетаскивания.

Так что dragEnter не подойдет, dargOver лучше всего подходит для этого.

При перетаскивании поверх первой половины перетаскиваемого элемента точка вставки заполнителя будет находиться перед перетаскиваемым элементом, а при перетаскивании поверх второй половины — после. (см. getBouldingClientRect, использование height / 2, конечно, если необходимо учитывать ширину перетаскивания по горизонтали).

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

Следующий фрагмент демонстрирует способ сделать это со следующими изменениями в вашем исходном коде:

  • Избежал многочисленных ссылок на переменные, поместив все в состояние, особенно потому, что их изменение повлияет на пользовательский интерфейс (потребуется повторный запуск).
  • Избегайте отдельных вызовов useState, помещая все переменные в общую переменную состояния и общий модификатор setState
  • Во избежание ненужных изменений состояния строк var, строки должны меняться только при завершении перетаскивания, так как об этом легче рассуждать => заполнитель на самом деле не является частью данных, он служит цели только в пользовательском интерфейсе
  • Избегая определения обработчика в коде рендеринга onEvent={() => handler(someVar)} с помощью ключа dataset data-drag-index , индекс может быть восстановлен после использования этого ключа: const index = element.dataset.dragIndex . Обработчик может работать только с событием, которое передается автоматически.
  • Избежать повторного создания (с точки зрения дочерних элементов) этих обработчиков при каждом рендеринге с помощью React.useCallback.

Различные добавленные классы css показывают текущее состояние каждого элемента, но не служат никакой функциональной цели.

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

Редактировать: переработано и исправлено полностью рабочее решение, обрабатывающее все протестированные крайние случаи

 const App = () => {
  const [state,setState] = React.useState({
    rows: [
      {name: 'foo'},
      {name: 'bar'},
      {name: 'baz'},
      {name: 'kazoo'}
    ],
    draggedIndex: -1,
    overIndex: -1,
    overZone: null,
    placeholderIndex: -1
  });
  const { rows, draggedIndex, overIndex, overZone, placeholderIndex } = state;
  const handleDragStart = React.useCallback((evt) => {
    const index = indexFromEvent(evt);
    setState(s => ({ ...s, draggedIndex: index }));
  });

  const handleDragOver = React.useCallback((evt) => {
  var rect = evt.target.getBoundingClientRect();
      var x = evt.clientX - rect.left; // x position within the element.
      var y = evt.clientY - rect.top;  // y position within the element.
    // dataset variables are strings
    const newOverIndex = indexFromEvent(evt);
    const newOverZone = y <= rect.height / 2 ? 'top' : 'bottom';
    
    const newState = { ...state, overIndex: newOverIndex, overZone: newOverZone }
    let newPlaceholderIndex = placeholderIndexFromState(newOverIndex, newOverZone);
    // if placeholder is just before (==draggedIndex) or just after (===draggedindex   1) there is not need to show it because we're not moving anything
    if (newPlaceholderIndex === draggedIndex || newPlaceholderIndex === draggedIndex   1) {
        newPlaceholderIndex = -1;
    }
    const nonFonctionalConditionOnlyForDisplay = overIndex !== newOverIndex || overZone !== newOverZone;
    // only update if placeholderIndex hasChanged
    if (placeholderIndex !== newPlaceholderIndex || nonFonctionalConditionOnlyForDisplay) {
      newState.placeholderIndex = newPlaceholderIndex;
      setState(s => ({ ...s, ...newState }));
    }
  });
  const handleDragEnd = React.useCallback((evt) => {
    const index = indexFromEvent(evt);
    // we know that much: no more dragged item, no more placeholder
    const updater = { draggedIndex: -1, placeholderIndex: -1,overIndex: -1, overZone: null };
    if (placeholderIndex !== -1) {
      // from here rows need to be updated
      // copy rows
      updater.rows = [...rows];
      // mutate updater.rows, move item at dragged index to placeholderIndex
      if (placeholderIndex > index) {
      // inserting after so removing the elem first and shift insertion index by -1
        updater.rows.splice(index, 1);
        updater.rows.splice(placeholderIndex - 1, 0, rows[index]);
      } else {
        // inserting before, so do not shift
        updater.rows.splice(index, 1);
        updater.rows.splice(placeholderIndex, 0, rows[index]);
      }
    }
    setState(s => ({
      ...s,
      ...updater
    }));
  });
  

  const renderedRows = rows.map((row, index) =>  (
    <div
      key={row.name}
      data-drag-index={index}
      className={
        `row ${
          index === draggedIndex
            ? 'dragged-row'
            : 'normal-row'}`
      }
      draggable
      onDragStart={handleDragStart}
      onDragOver={handleDragOver}
      onDragEnd={handleDragEnd}
    >
      {row.name}
    </div>
  ));
  // there is a placeholder to show, add it to the rendered rows
  if (placeholderIndex !== -1) {
    renderedRows.splice(
      placeholderIndex,
      0,
      <Placeholder />
    );
  }
  return (
    <div>
      {renderedRows}
      <StateDisplay state={state} />
    </div>
  );
};

const Placeholder = ({ index }) => (
    <div
      key="placeholder"
      className="row placeholder-row"
    ></div>
  );
function indexFromEvent(evt) {
    try {
        return parseInt(evt.target.dataset.dragIndex, 10);
    } catch (err) {
       return -1;
    }
}
function placeholderIndexFromState(overIndex, overZone) {
    if (overZone === null) {
      return;
    }
    if (overZone === 'top') {
      return overIndex;
    } else {
      return overIndex   1;
    }
}
const StateDisplay = ({ state }) => {
  return (
    <div className="state-display">
      {state.rows.map(r => r.name).join()}<br />
      draggedIndex: {state.draggedIndex}<br />
      overIndex: {state.overIndex}<br />
      overZone: {state.overZone}<br />
      placeholderIndex: {state.placeholderIndex}<br />
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById('root')); 
 .row { width: 100px; height: 30px; display: flex; align-items: center; justify-content: center; }
.row:nth-child(n 1) {  margin-top: 5px; }
.row.normal-row { background: #BEBEBE; }
.row.placeholder-row { background: #BEBEFE; }
.row.normal-row:hover { background: #B0B0B0; }
.row.placeholder-row:hover { background: #B0B0F0; }

.row.dragged-row { opacity: 0.3; background: #B0B0B0; }
.row.dragged-row:hover { background: #B0B0B0; }

.state-display { position: absolute; right: 0px; top: 0px; width: 200px; } 
 <html><body><div id="root"></div><script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0-alpha.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0-alpha.0/umd/react-dom.production.min.js"></script></body></html>