-
Notifications
You must be signed in to change notification settings - Fork 185
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Questionnaire: Drag and Drop "Sorting" question #323936
- Loading branch information
lamtranb
committed
Mar 8, 2023
1 parent
1e39079
commit 2f41b65
Showing
44 changed files
with
2,255 additions
and
55 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Oops, something went wrong.