вступление

Вы уже пробовали реализовать механизм перетаскивания с помощью JavaScript? Если да, то вы должны знать, что в основном речь идет о перемещении элемента DOM и всех его дочерних элементов из одного места в другое. Но пытались ли вы переместить столбец таблицы из одного места в другое?
Если вы ответили нет хотя бы на один из предыдущих вопросов, возможно, вас заинтересуют следующие части этой статьи. Особенно по второму вопросу, где вы явно не найдете столько ресурсов в Интернете, сколько перетаскивание строк в таблицах.
Но прежде всего позвольте мне объяснить, почему мне пришлось решать эту проблему для Esker.
Наше основное решение направлено на то, чтобы дать нашим клиентам возможность обрабатывать свои бизнес-документы быстрее и эффективнее. Это означает, что он должен быть полностью настраиваемым. Несколько недель назад я решил переработать способ разработки бизнес-отчетности в нашем веб-интерфейсе, и я думал о том, как максимально легко спроектировать табличную часть отчетности. На этом этапе мы хотим, чтобы пользователь предоставил свой выбор данных, по которым он хочет сообщить, что для него наиболее важно. Этот вопрос приоритета подразумевает сортировку столбцов таблицы, и именно здесь возникает суть данной статьи.

Отказ от ответственности за совместимость браузеров

Разработка интерфейса веб-клиента всегда подразумевает один и тот же вопрос: будет ли он работать во всех браузерах и во всех версиях?
Когда вы делаете это для редактора программного обеспечения, как я, ответ должен быть да, но в личных целях вы может проигнорировать этот момент, поэтому я попытаюсь объяснить, где вы можете игнорировать некоторые из моих решений.
Я также решил использовать некоторые помощники jQuery, в основном для легкого управления DOM. При необходимости вы можете переписать эти части в ванильном стиле.
Самым низким требованием к используемому браузеру является поддержка HTML5 и особенно событий перетаскивания. Проверить этот ключевой момент можно, например, с помощью полезной библиотеки Modernizer.

Давайте начнем

Первый шаг довольно прост, потому что все, что вам нужно сделать, это определить перетаскиваемый элемент DOM. Это достигается добавлением к первому атрибуту draggable = true. Разве это не легко?
Что ж, если вы попробуете напрямую, вы можете заметить, что могут потребоваться некоторые хитрости CSS. По крайней мере, вы должны изменить курсор с помощью следующей директивы cursor: move.
Теперь вы могли бы сказать что-то вроде «это смешно, но совершенно бесполезно». И вы были бы правы, но помните, что я писал о JavaScript, поэтому, возможно, нужно добавить прослушиватель событий, не так ли?

Послушай меня

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

function listenerDragStart(e) {
  e.target.style.opacity = '0.4';  // e.target = this
}

Теперь нам нужно просто добавить прослушиватель событий к нашим перетаскиваемым объектам. Поскольку наша основная цель - перетаскивать столбцы таблицы, следующее должно работать:

var cols = document.querySelectorAll('th');
[].forEach.call(cols, function(col) {
  col.addEventListener('dragstart', listenerDragStart, false);
});

Симметрично, как только событие перетаскивания закончено, вы должны отменить все сделанные вами изменения визуального аспекта. Вы, наверное, уже догадались, что это можно сделать с помощью прослушивателя событий dragend:

function listenerDragEnd(e) {
  e.target.style.opacity = '1.0';  // e.target = this
}

Помогите своим пользователям

Перетаскивание стало естественным действием, но оно сопровождалось визуальными кодами, которые необходимо соблюдать, чтобы не потерять пользователей. Один из этих кодов - всегда видеть, куда вы собираетесь уронить свой элемент. Без этого пользователи начинают задаваться вопросом, хорошо ли учтено их перетаскивание. Здесь помогут события dragenter, dragover и dragleave.

Событие dragenter позволит вам показать, что целевой элемент является зоной перетаскивания. Опять же, вы можете изменить стиль напрямую или просто добавить класс CSS в зависимости от того, сколько стилей вы хотите сделать.
Событие dragover будет тем, куда вы переносите исходный элемент DOM к месту назначения. Мы вернемся к этому позже в разделе dataTransfer. Между тем, если вам интересно, почему вы не меняете стиль здесь, ответ - уменьшить ненужный рендеринг. Действительно, событие dragover вызывается, пока столбец завис.
Событие dragleave, как и событие dragend для dragstart one используется для возврата визуальных эффектов, добавленных во время dragenter.

function listenerDragOver(e) {
  if (e.preventDefault) {
    e.preventDefault(); // Necessary. Allows us to drop.
  }
  return false;
}
function listenerDragEnter(e) {
    e.target.classList.add('dropZone');
}
function listenerDragLeave(e) {
  e.target.classList.remove('dropZone');
}

Перетаскивание теперь довольно круто, но как я могу правильно бросить?

Как сказал Альфред Ланнинг: это правильный вопрос. Действительно, мы говорили о том, как управлять событием перетаскивания, но не о том, как отбрасывать перетаскиваемые данные. Здесь свою роль играет хорошо названный объект dataTransfer.
Если вы посмотрите его документацию, вы увидите, что этот объект имеет множество свойств и методов, но не содержит все поддерживается всеми браузерами. Как отмечалось ранее, я сосредоточусь только на тех, которые требуются для этой темы и поддерживаются почти всеми браузерами HTML5.
Во-первых, мы должны определить, что мы хотим передать от начала перетаскивания до начала уронить. Здесь я решил использовать заголовок моей перетаскиваемой колонки, поскольку это простой способ объяснить механизм. Для этого мы будем использовать метод setData, который принимает два аргумента: тип данных и сами данные. К сожалению, Internet Explorer поддерживает только текстовый тип данных, поэтому мы постараемся его использовать.
Во-вторых, мы должны получить эти данные, и вы уже догадались, что мы будет использовать метод getData. Он принимает только один аргумент, который является типом данных, который мы хотим получить.
Наконец (для этого примера) мы можем определить эффекты, разрешенные во время перетаскивания. Здесь мы будем использовать только эффект движения
Вот результат для меня:

function listenerDragStart(e) {
    e.dataTransfer.effectAllowed = 'move';
    var srcTxt = $(this).text();
    e.dataTransfer.setData('text', $(this).text());
}
 function listenerDrop(e) {
    if (e.preventDefault) {
        e.preventDefault();
    }
    if (e.stopPropagation) {
        e.stopPropagation();
    }
    var srcTxt = e.dataTransfer.getData('text'); 
    var destTxt = $(this).text();
    if (srcTxt != destTxt) {
        //Search for the source column in order to move it at the destination index
        var dragSrcEl = $(".table th:contains(" + srcTxt + ")");
        var srcIndex = dragSrcEl.index() + 1;
        var destIndex = $("th:contains(" + destTxt + ")").index() + 1;
        dragSrcEl.insertAfter($(this));
        $.each($('.table tr td:nth-child(' + srcIndex + ')'), function (i, val) {
            var index = i + 1;
            $(this).insertAfter($('.table tr:nth-child(' + index + ') td:nth-child(' + destIndex + ')'));
        });
    }
    return false;
}

На шаг впереди?

Мы можем точно! Еще один способ помочь пользователю в его действиях - показать ему не только заголовок столбца, но и все строки. К сожалению, следующий код не будет поддерживаться IE, но он будет классным во всех других браузерах.
Чтобы отобразить весь столбец, мы будем использовать метод setDragImage объекта DataTransfer. Вы должны заметить, что объект DOM, передаваемый этому методу, должен быть видимым в вашей DOM или поступать из внешнего источника. Здесь мы клонируем столбец нашей таблицы, но этот клон должен быть отрисован. Это означает, что все уловки, основанные на свойствах видимости или отображения, не сработают.
Вот как я создал призрак моей колонки в обработчике dragstart:

//The only IE version supported in our product is 11 but there are many other ways to detect this browser
var isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
if (!isIE11) {
        var srcIndex = $("th:contains(" + srcTxt + ")").index() + 1;
        //Create column's container
        var dragGhost = document.createElement("table");
        dragGhost.classList.add("tableGhost");
        dragGhost.classList.add("table-bordered");
        //in order tor etrieve the column's original width
        var srcStyle = document.defaultView.getComputedStyle(this);
        dragGhost.style.width = srcStyle.getPropertyValue("width");
        
        //Create head's clone
        var theadGhost = document.createElement("thead");
        var thisGhost = this.cloneNode(true);
        thisGhost.style.backgroundColor = "red";
        theadGhost.appendChild(thisGhost);
        dragGhost.appendChild(theadGhost);
        //Create body's clone
        var tbodyGhist = document.createElement("tbody");
        $.each($('.table tr td:nth-child(' + srcIndex + ')'),         function (i, val) {
            var currentTR = document.createElement("tr");
            var currentTD = document.createElement("td");
            currentTD.innerText = $(this).text();
            currentTR.appendChild(currentTD);
            tbodyGhist.appendChild(currentTR);
        });
        dragGhost.appendChild(tbodyGhist);
        
        //Hide ghost
        dragGhost.style.position = "absolute";
        dragGhost.style.top = "-1500px";
        
        document.body.appendChild(dragGhost);
        e.dataTransfer.setDragImage(dragGhost, 0, 0);
    }

Недостаточно?

Вы можете пойти дальше в стилях и других подобных вещах (отображение зоны, в которую будет добавлен столбец, не всегда опускаться после зависшего столбца…), но в моем случае этого показалось достаточно. Не стесняйтесь улучшать этот образец, но я по крайней мере надеюсь, что он помог вам лучше понять перетаскивание столбцов таблицы.
Вы можете получить демонстрацию моего кода в этой скрипке.