Skip to content

Commit

Permalink
Questionnaire: Drag and Drop "Sorting" question #323936
Browse files Browse the repository at this point in the history
  • Loading branch information
lamtranb committed Mar 8, 2023
1 parent 1e39079 commit 2f41b65
Show file tree
Hide file tree
Showing 44 changed files with 2,255 additions and 55 deletions.
10 changes: 10 additions & 0 deletions amd/build/sorting.min.js

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

1 change: 1 addition & 0 deletions amd/build/sorting.min.js.map

Large diffs are not rendered by default.

355 changes: 355 additions & 0 deletions amd/src/sorting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Sorting question for display and ordering by keycodes arrow.
*
* @copyright 2022 The Open University.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
*/

let dragElement; // Drag element where stored the temp element for drag and drop.
let startQId; // Current question id for stored data to database when user saves the data.
let questions = document.querySelectorAll(".draggable.qn-sorting-list__items") || []; // Get list of answers.
let sortingListItems = new Map(); // Storing list of answers for mobile mode and arrow keys.
let currentPosition; // Get current position when the mouse moves for frozen the event DragEnter.

/**
* Handle on start drag the element.
*
* @param {Event} e event when user click and drag element.
*/
function handleOnDragStart(e) {
this.style.opacity = isSetOpacity(0.01);
dragElement = this;
startQId = getDataQId(this);
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/html", this.innerHTML);
}

/**
* Handle on drag end element.
*/
function handleOnDragEnd() {
this.style.opacity = isSetOpacity(1);
questions.forEach((item) => {
item.classList.remove("over");
item.style.opacity = isSetOpacity(1);
});
}

/**
* Handle on drag over to another element.
*
* @param {Event} e event when drop element.
* @return {Boolean} false is default.
*/
function handleOnDragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
const qIdItem = getDataQId(this);
if (startQId === qIdItem) {
this.style.opacity = isSetOpacity(0);
}
return false;
}

/**
* Handle on drag enter element.
*
* @param {Event} e event when user drag into another element.
*/
function handleOnDragEnter(e) {
this.classList.add("over");
const qIdItem = getDataQId(this);
const {innerHTML: currentHTML, style: currentStyle} = this;
const {innerHTML: dragHTML, style: dragStyle} = dragElement;

// Prevent the elements switch in multiple time.
if (e.clientX === currentPosition?.x && e.clientY === currentPosition?.y) {
return;
}
if (currentHTML !== dragHTML && startQId === qIdItem) {
currentPosition = {
x: e.clientX,
y: e.clientY
};
dragElement.innerHTML = currentHTML;
dragElement.style = currentStyle;
this.style = dragStyle;
this.innerHTML = dragHTML;
dragElement = this;
e.dataTransfer.dropEffect = "move";
e.dataTransfer.setData("text/html", this.innerHTML);
questions.forEach((item) => {
item.style.opacity = isSetOpacity(1);
});
}
}

/**
* Handle on drag leave element.
*/
function handleOnDragLeave() {
this.classList.remove("over");
}

/**
* Handle on drop element.
*
* @param {Event} e event when drop element.
* @return {Boolean} false is default.
*/
function handleOnDrop(e) {
if (e.preventDefault) {
e.preventDefault();
}
if (e.stopPropagation) {
e.stopPropagation();
}
const qIdItem = getDataQId(this);
if (qIdItem === startQId) {
// Clearing data drag element.
e.dataTransfer.clearData("text/html");
questions.forEach((item) => {
item.style.opacity = "unset";
item.style.boxShadow = "unset";
});
}
return false;
}

/**
* Set opacity for specific browsers.
*
* @param {Number} value of opacity.
* @return {String|Number} number or "unset" value.
*/
function isSetOpacity(value) {
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
if (isFirefox) {
return 'unset';
}
const isSafari = navigator.userAgent.toLowerCase().indexOf('safari') > -1;
if (isSafari && value !== 0) {
return 1;
}
return value;
}

/**
* Get data question id.
*
* @param {DOMElement} element question.
* @returns {Number} qid of question.
*/
function getDataQId(element) {
return element?.getAttribute("data-qid");
}

/**
* Get data index of answer.
*
* @param {DOMElement} element question.
* @returns {number} dataIndex of answer.
*/
function getDataQIndex(element) {
return Number(element?.getAttribute("data-index"));
}

/**
* Moving the quesiton by key.
*
* @param {DOMEvent} e event user press arrow key.
* @returns {void}
*/
function moveQuestion(e) {
let qId = getDataQId(this);
let currentAnswer = getDataQIndex(this);

if (e.key === "ArrowDown" || e.key === "ArrowRight") {
let nextAnswer = currentAnswer + 1;
if (nextAnswer < sortingListItems.get(qId).length) {
swapItems(currentAnswer, nextAnswer, qId);
sortingListItems.get(qId)[nextAnswer].focus();
}
e.preventDefault();
}

if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
let prevAnswer = currentAnswer - 1;
if (prevAnswer >= 0) {
swapItems(currentAnswer, prevAnswer, qId);
sortingListItems.get(qId)[prevAnswer].focus();
}
e.preventDefault();
}
}

/**
* Swap list items that are drag and drop.
*
* @param {number} fromIndex index of element.
* @param {number} toIndex index of element.
* @param {number} qId question id of question.
*/
function swapItems(fromIndex, toIndex, qId) {
const {innerHTML: itemOneHtml, style: itemOneStyle} = sortingListItems.get(qId)[fromIndex];
const {innerHTML: itemTwoHtml, style: itemTwoStyle} = sortingListItems.get(qId)[toIndex];
sortingListItems.get(qId)[fromIndex].innerHTML = itemTwoHtml;
sortingListItems.get(qId)[fromIndex].style = itemTwoStyle;
sortingListItems.get(qId)[toIndex].innerHTML = itemOneHtml;
sortingListItems.get(qId)[toIndex].style = itemOneStyle;
}

// Variables for mobile mode.
let newPosX = 0,
newPosY = 0,
startPosX = 0,
startPosY = 0,
startFromIndex,
defaultBorder = "1px solid #000",
elementTouch,
styleElement;

/**
* Touching start the quesiton in mobile.
*
* @param {DOMEvent} e event user press arrow key.
*/
function touchStart(e) {
// Set default position of element of toucher.
startPosX = e.touches[0].clientX - this.offsetLeft;
startPosY = e.touches[0].clientY - this.offsetTop;

startFromIndex = getDataQIndex(this);
startQId = getDataQId(this);
const {width, height} = window.getComputedStyle(this);
styleElement = {
width: parseInt(width.split("px")[0]),
height: parseInt(height.split("px")[0])
};

// Copy new elemenent.
elementTouch = this;
this.children[0].addEventListener("touchmove", touchMove);
}

/**
* Touching move the quesiton in mobile.
*
* @param {DOMEvent} e event user press arrow key.
*/
function touchMove(e) {
e.preventDefault();
// Set the new position when the user holds and moves the touching.
newPosX = e.touches[0].clientX - startPosX + "px";
newPosY = e.touches[0].clientY - startPosY + "px";

// Set border none when user move.
elementTouch.style.border = "none";

// Set style css for moving element.
this.style.border = defaultBorder;
this.style.background = "inherit";
this.style.left = newPosX;
this.style.top = newPosY;
this.style.position = "absolute";
this.style.width = styleElement.width + 1;
this.style.height = styleElement.height + 1;

// Call touching end event when the user stops the touching.
this.addEventListener("touchend", touchEnd);
}

/**
* Touching end the quesiton in mobile.
*
* @param {DOMEvent} e event user touch end.
*/
function touchEnd(e) {
// Clearing the style css of element"s move.
this.parentNode.style.border = defaultBorder;
this.style.left = "inherit";
this.style.top = "inherit";
this.style.border = "unset";
this.style.position = "inherit";
this.style.width = "inherit";
this.style.height = "inherit";

// Get current element at the point.
const element = document.elementFromPoint(
e.changedTouches[0]?.clientX,
e.changedTouches[0]?.clientY);

const item = element.closest(".qn-sorting-list li");

// Ordering elements.
const qId = getDataQId(item || null);
if (item && startQId === qId) {
const toIndex = getDataQIndex(item);
if (startFromIndex >= 0 && qId && toIndex >= 0) {
swapItems(startFromIndex, toIndex, qId);
for (let i = toIndex - 1; i > startFromIndex; i--) {
swapItems(startFromIndex, i, qId);
}
}
}
}

/**
* Initialize events.
*/
function addEventListeners() {
// Binding event for arrow key and touched in mobile.
sortingListItems.forEach((elements, qid) => {
[...elements].forEach((element, index) => {
element.setAttribute("data-index", index);
element.setAttribute("data-qid", qid);
element.addEventListener("keydown", moveQuestion);
element.addEventListener("touchstart", touchStart);
});
});

// Binding event drag and drop for element.
if (questions.length > 0) {
questions.forEach((question) => {
question.addEventListener("dragstart", handleOnDragStart, false);
question.addEventListener("dragend", handleOnDragEnd, false);
question.addEventListener("dragover", handleOnDragOver, false);
question.addEventListener("dragenter", handleOnDragEnter, false);
question.addEventListener("dragleave", handleOnDragLeave, false);
question.addEventListener("drop", handleOnDrop, false);
});
}

}

/**
* Initialize function to get all the elements ready to serve the drag and drop question.
*/
export const init = () => {
document.querySelectorAll(".draggable.qn-sorting-list__items img").forEach(item => {
item.setAttribute("draggable", false);
});

const sortingList = document.getElementsByClassName("qn-sorting-list");
if ([...sortingList].length > 0) {
[...sortingList].forEach((question) => {
sortingListItems.set(getDataQId(question), question.children);
});
}
addEventListeners();
};
File renamed without changes.
Loading

0 comments on commit 2f41b65

Please sign in to comment.