Skip to content

Commit

Permalink
10/UI/DataTable-Ordering touch screen drag and drop 44136
Browse files Browse the repository at this point in the history
  • Loading branch information
catenglaender committed Feb 25, 2025
1 parent 5fc406b commit ab80e84
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 37 deletions.
2 changes: 1 addition & 1 deletion components/ILIAS/UI/resources/js/Table/dist/table.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

172 changes: 146 additions & 26 deletions components/ILIAS/UI/resources/js/Table/src/orderingtable.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,30 @@ export default class OrderingTable {
#table;

/**
* @type {array<HTMLTableRowElement>}
* @type {Array<HTMLTableRowElement>}
*/
#rows;

/**
* The original row that is being dragged.
* @type {HTMLTableRowElement}
*/
#tmpDragRow;
#originRow;

/**
* @param {string} tableId
* The clone that will follow the cursor (drag image).
* @type {HTMLTableRowElement}
*/
#dragImage;

/**
* The placeholder inserted into the table.
* @type {HTMLTableRowElement}
*/
#placeholderRow;

/**
* @param {string} componentId
* @throws {Error} if DOM element is missing
*/
constructor(componentId) {
Expand All @@ -47,56 +60,163 @@ export default class OrderingTable {
if (this.#table === null) {
throw new Error('There is no <table> in the component\'s HTML.');
}

this.#indexRows();
this.#rows.forEach((row) => this.#addDraglisteners(row));
}

#indexRows() {
this.#rows = Array.from(this.#table.rows);
this.#rows.shift();// exclude header
this.#rows.pop();// exclude footer
this.#rows.shift(); // exclude header
this.#rows.pop(); // exclude footer
}

#addDraglisteners(row) {
row.setAttribute('draggable', true);
row.addEventListener('dragstart', (event) => this.dragstart(event));
row.addEventListener('dragover', (event) => this.dragover(event));
row.addEventListener('dragend', () => { this.#indexRows(); this.#renumberAfterDrag(); });
row.addEventListener('dragend', (event) => this.dragend(event));

row.addEventListener('touchstart', (event) => this.touchstart(event));
row.addEventListener('touchmove', (event) => this.touchmove(event));
row.addEventListener('touchend', (event) => this.touchend(event));
row.addEventListener('touchcancel', (event) => this.touchend(event));
}

dragstart(event) {
this.#tmpDragRow = event.target;
}
this.#component.classList.add('dragInProgress');
// Safari requires setting some data
event.dataTransfer.setData('text/plain', 'dummy');

#isDraggedElementValidRow() {
return this.#rows.includes(this.#tmpDragRow);
this.#originRow = event.target.closest('tr');

// Create the drag image - cannot use pure browser default because of Safari
this.#dragImage = this.#originRow.cloneNode(true);
this.#dragImage.classList.add('dragImage');
this.#dragImage.style.top = '-9999px';
this.#component.appendChild(this.#dragImage);
event.dataTransfer.setDragImage(this.#dragImage, 0, 0);

// Create the placeholder gap between rows to indicate where the item will be dropped
this.#placeholderRow = this.#originRow.cloneNode(true);
this.#placeholderRow.classList.add('placeholderRow');
Array.from(this.#placeholderRow.getElementsByTagName('td'))
.forEach((cell) => {
cell.innerHTML = '';
}
);

// an indicator at the position where the item was before
this.#originRow.classList.add('dragOrigin');
}

dragover(event) {
if (!this.#isDraggedElementValidRow()) {
return;
if (!this.#isDraggedElementValidRow()) return;
event.preventDefault();
const target = event.target.closest('tr');
if (target && target !== this.#placeholderRow) {
if (this.#rows.indexOf(target) > this.#rows.indexOf(this.#originRow)) {
target.after(this.#placeholderRow);
} else {
target.before(this.#placeholderRow);
}
}
}

const e = event;
e.preventDefault();
const target = e.target.closest('tr');
dragend(event) {
event.preventDefault();
this.#component.removeChild(this.#dragImage);
this.#placeholderRow.replaceWith(this.#originRow); // drop original where placeholder was
this.#originRow.classList.remove('dragOrigin');
this.#originRow.classList.add('dragSettle');
// this is for the duration of the CSS settle animation
setTimeout(
() => { this.#originRow.classList.remove('dragSettle'); },
200,
);
this.#component.classList.remove('dragInProgress');
this.#indexRows();
this.#renumberAfterDrag();
}

if (this.#rows.indexOf(target) > this.#rows.indexOf(this.#tmpDragRow)) {
target.after(this.#tmpDragRow);
#isDraggedElementValidRow() {
return this.#rows.includes(this.#originRow);
}

// Drag and Drop on touch devices
touchstart(event) {
this.#originRow = event.target.closest('tr');

this.#placeholderRow = this.#originRow.cloneNode(true);
this.#placeholderRow.classList.add('placeholderRow');
Array.from(this.#placeholderRow.getElementsByTagName('td'))
.forEach(
(cell) => {
cell.innerHTML = '';}
);

// drag image for touch
this.#dragImage = this.#originRow.cloneNode(true);
this.#dragImage.classList.add('touchDragImage');
this.#component.appendChild(this.#dragImage);

this.#originRow.classList.add('dragOrigin');
}

touchmove(event) {
event.preventDefault();
const touch = event.touches[0];

this.#dragImage.style.left = `${touch.clientX - 50}px`;
this.#dragImage.style.top = `${touch.clientY}px`;

const target = document.elementFromPoint(touch.clientX, touch.clientY)?.closest('tr');
if (target && this.#rows.includes(target)) {
if (this.#rows.indexOf(target) > this.#rows.indexOf(this.#originRow)) {
target.after(this.#placeholderRow);
} else {
target.before(this.#placeholderRow);
}
}

// Scroll viewport if near edges
const buffer = 100;
const scrollStep = 8;
if (touch.clientY < buffer) {
this.#startScrolling(-scrollStep);
} else if (touch.clientY > window.innerHeight - buffer) {
this.#startScrolling(scrollStep);
} else {
target.before(this.#tmpDragRow);
this.#stopScrolling();
}
}

touchend() {
this.#component.removeChild(this.#dragImage);
this.#placeholderRow.replaceWith(this.#originRow);
this.#originRow.classList.remove('dragOrigin');
this.#indexRows();
this.#renumberAfterDrag();
}

#renumberAfterDrag() {
let pos = 10;
this.#table.querySelectorAll('input[type="number"]').forEach(
(input) => {
const field = input;
field.value = pos;
pos += 10;
},
);
this.#table.querySelectorAll('input[type="number"]').forEach((input) => {
input.value = pos;
pos += 10;
});
}

#startScrolling(step) {
if (this.scrollInterval) return;
this.scrollInterval = setInterval(() => {
window.scrollBy(0, step);
}, 16);
}

#stopScrolling() {
if (this.scrollInterval) {
clearInterval(this.scrollInterval);
this.scrollInterval = null;
}
}
}
Loading

0 comments on commit ab80e84

Please sign in to comment.