Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added drag and drop element and example questions #23

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions elements/pl-drag-and-drop-grid/drag-and-drop-grid.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
.draggable-items {
margin-bottom: 20px;
}

.draggable {
padding: 5px;
margin: 5px;
display: inline-block;
background-color: lightgray;
border: 1px solid #000;
cursor: grab;
}

#grid-container {
display: flex; /* This makes sure the container is a flexbox */
flex-direction: column; /* Aligns the child grid-rows vertically */
align-items: center; /* Centers the grid rows */
margin: 0 auto;
}

.grid-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(50px, 1fr)); /* Adjust columns based on content width */
gap: 1px;
}

.grid-cell {
border: 1px solid #000;
background-color: #fff;
min-height: 50px;
min-width: 50px;
display: flex;
justify-content: center;
align-items: center;
padding: 5px; /* This should apply to all sides */
box-sizing: border-box; /* Include padding and border in the element's size */
}

.blocked {
background-color: #d9534f; /* Some color to indicate blockage */
}

.draggable.in-grid {
cursor: move; /* Change cursor to indicate moveability */
}
249 changes: 249 additions & 0 deletions elements/pl-drag-and-drop-grid/drag-and-drop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import lxml.html
import prairielearn as pl
import ast

# Constants for default values
DEFAULT_NUM_COLS = 4
DEFAULT_NUM_ROWS = 4
DEFAULT_BLOCKED_CELLS = ''
DEFAULT_ITEMS = ''
DEFAULT_POOL_TYPE = "finite"
DEFAULT_ANSWER = ''
PARTIAL_CREDIT = False

def prepare(element_html, data):
element = lxml.html.fragment_fromstring(element_html)
data['params']['num_rows'] = pl.get_integer_attrib(element, "num_rows", DEFAULT_NUM_ROWS)
data['params']['num_cols'] = pl.get_integer_attrib(element, "num_cols", DEFAULT_NUM_COLS)
data['params']['items'] = pl.get_string_attrib(element, "items", DEFAULT_ITEMS)
data['params']['pool_type'] = pl.get_string_attrib(element, "pool_type", DEFAULT_POOL_TYPE)
data['params']['blocked_cells'] = pl.get_string_attrib(element, "blocked_cells", DEFAULT_BLOCKED_CELLS)

def render(element_html: str, data: pl.QuestionData) -> str:
num_rows = data['params']['num_rows']
num_cols = data['params']['num_cols']
pool_type = data['params']['pool_type']
blocked_cells_input = data['params']['blocked_cells']
if pool_type == "infinite":
data_infinite = "true"

else:
data_infinite = "false"

if blocked_cells_input.strip(): # Checks if the string is not empty and not just whitespace
try:
blocked_cells = ast.literal_eval(blocked_cells_input)
except SyntaxError as e:
print(f"Error parsing blocked_cells: {e}")
blocked_cells = []
else:
blocked_cells = []
items = [item.strip() for item in data['params']['items'].split(',')]

# Building the grid HTML
grid_html = '<div id="grid-container">\n'
for row in range(num_rows):
grid_html += f'<div class="grid-row" style="grid-template-columns: repeat({num_cols}, 50px);">\n'
for col in range(num_cols):
cell_class = "grid-cell"
if (row, col) in blocked_cells:
cell_class = "blocked"
grid_html += f'<div class="{cell_class}" data-row="{row}" data-col="{col}"></div>\n'
grid_html += '</div>\n'
grid_html += '</div>\n'

# Building the items HTML
items_html = '<div class="draggable-items">\n'
for item in items:
items_html += f'<div class="draggable" draggable="true" data-infinite="{data_infinite}" id="{item}">{item}</div>\n'
items_html += '</div>\n'

javascript_function = """
<script>
document.addEventListener('DOMContentLoaded', (event) => {{

// Initialize the grid state as a 2D array filled with empty strings
let grid_state = Array.from({{ length: {num_rows} }}, () => Array.from({{ length: {num_cols} }}, () => ''));

function updateGridState() {{
document.getElementById("grid_state").value = JSON.stringify(grid_state);
console.log(grid_state);
}}

function dragStart(event) {{
// Set the id of the draggable element as the data to be transferred
event.dataTransfer.setData('text/plain', event.target.id);
const isInfiniteSource = event.target.hasAttribute('data-infinite');
event.dataTransfer.setData('isInfiniteSource', isInfiniteSource);
}}

function dragOver(event) {{
event.preventDefault();
}}




function drop(event) {{
event.preventDefault();
const id = event.dataTransfer.getData('text/plain');
let draggable = document.getElementById(id);
let targetCell = event.target.closest('.grid-cell');

if (targetCell.classList.contains('blocked')) {{
console.log('This cell is blocked.');
return;
}}



let newRow = parseInt(targetCell.dataset.row);
let newCol = parseInt(targetCell.dataset.col);

if (draggable.getAttribute('data-infinite') === 'true') {{
console.log("should clone");
draggable = draggable.cloneNode(true);
draggable.removeAttribute('data-infinite');
draggable.id = `clone-${{id}}-${{Date.now()}}`;
draggable.addEventListener('dragstart', dragStart);
draggable.addEventListener('click', removeItemFromCell);
}}

if (!targetCell.hasChildNodes()) {{
// Clear previous location in grid state if applicable
if (draggable.dataset.row && draggable.dataset.col) {{
grid_state[draggable.dataset.row][draggable.dataset.col] = '';
}}

// Update new location in grid state
draggable.dataset.row = newRow;
draggable.dataset.col = newCol;
targetCell.appendChild(draggable);
draggable.classList.add('in-grid');
grid_state[newRow][newCol] = draggable.textContent;

// Ensure the grid state is updated properly
updateGridState();
}}
}}


function mouseEnter(event) {{
// This function adds a red X to the draggable item
const draggable = event.target;
const overlay = document.createElement('span'); // Create a new span element for the overlay
overlay.textContent = 'X'; // Set the text content of the span to 'X'
overlay.style.color = 'red'; // Set the color of the text to red
overlay.style.position = 'absolute'; // Position absolutely to overlay on the draggable
overlay.style.right = '5px'; // Position towards the right of the container
overlay.style.top = '0'; // Position at the top of the container
overlay.style.fontSize = '24px'; // Set a larger font size for visibility
overlay.style.fontWeight = 'bold'; // Make the font bold
overlay.setAttribute('class', 'overlay-x'); // Add a class for potential additional styling
draggable.appendChild(overlay); // Append the overlay to the draggable
console.log('jerenter');
}}

function mouseOut(event) {{
// This function removes the red X from the draggable item
const draggable = event.target;
const overlay = draggable.querySelector('.overlay-x'); // Select the overlay span by its class
if (overlay) {{
draggable.removeChild(overlay); // Remove the overlay from the draggable
}}
console.log('jerout');
}}

function removeItemFromCell(event) {{
const draggableItem = event.target.closest('.draggable'); // Ensure the target is always the draggable item
if (draggableItem && draggableItem.classList.contains('draggable')) {{
const row = draggableItem.dataset.row;
const col = draggableItem.dataset.col;

// Clear the grid state at the original location
if (row !== undefined && col !== undefined) {{
grid_state[row][col] = '';
}}

// Move the item back to the draggable area
if ({data_infinite} === "finite") {{
document.querySelector('.draggable-items').appendChild(draggableItem);
}} else {{
draggableItem.remove();
}}

draggableItem.classList.remove('in-grid');

// Clear the dataset attributes
delete draggableItem.dataset.row;
delete draggableItem.dataset.col;

// Update the grid state to reflect the changes
updateGridState();
}}
}}


const draggables = document.querySelectorAll('.draggable');
draggables.forEach(draggable => {{
draggable.addEventListener('dragstart', dragStart);
//draggable.addEventListener('mouseenter', mouseEnter);
//draggable.addEventListener('mouseout', mouseOut)
draggable.addEventListener('click', removeItemFromCell);
}});

const gridCells = document.querySelectorAll('.grid-cell');
gridCells.forEach(cell => {{
cell.addEventListener('dragover', dragOver);
cell.addEventListener('drop', drop);
}});

updateGridState();
}});
</script>
""".format(num_rows=num_rows, num_cols=num_cols, pool_type=pool_type, data_infinite=data_infinite)

full_html = f'{items_html}{grid_html}{javascript_function}'
full_html += '<input type="hidden" id="grid_state" name="grid_state" value="">'

return full_html

def grade(element_html, data):
element = lxml.html.fragment_fromstring(element_html)
grid_state = eval(data["submitted_answers"]["grid_state"])
partial_credit = pl.from_json(element.get("partial_credit"))
correct_answer = eval(pl.get_string_attrib(element, "answer", DEFAULT_ANSWER))
num_rows = data['params']['num_rows']
num_cols = data['params']['num_cols']
num_items = 0
num_correct = 0
feedback = ""

for i in range(num_rows):
for j in range(num_cols):
if correct_answer[i][j] != '':
num_items += 1
if grid_state[i][j] == correct_answer[i][j]:
num_correct += 1
else:
feedback += f"{correct_answer[i][j]} is in the wrong place. "
elif grid_state[i][j] != '':
pass

if partial_credit == "False":
score = 0
if num_correct == num_items:
score = 1
else:
score = max(num_correct / num_items, 0)

if score == 1:
feedback += "All correct!"

data["partial_scores"]["score"] = {
"score": score,
"weight": 1,
"feedback": feedback
}

return data
6 changes: 6 additions & 0 deletions elements/pl-drag-and-drop-grid/info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"controller": "drag-and-drop.py",
"dependencies": {
"elementStyles": ["drag-and-drop-grid.css"]
}
}
10 changes: 10 additions & 0 deletions questions/pl-drag-and-drop-examples/drag_and_drop/info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"uuid": "a93eb347-bdff-420f-93ed-4c92f0bed8ba",
"title": "Drag and Drop",
"topic": "Template",
"tags": [
"mwest",
"fa17"
],
"type": "v3"
}
34 changes: 34 additions & 0 deletions questions/pl-drag-and-drop-examples/drag_and_drop/question.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hash Table Grid</title>
</head>
<body>

<p>
Assume that elements are hashed based on the index of the first letter (e.g. a = 0, b = 1).

Insert the following elements in order (from left to right).

In the grid below, the top row corresponds to the 0th bucket. The row beneath that corresponds to the 1st bucket.


</p>
<pl-question-panel>

<pl-drag-and-drop-grid
num_cols="3"
pool_type = "infinite"
num_rows="6"
blocked_cells="[]"
items='Air, Bun, Byun'
partial_credit = "True"
answer='[["Air", "", ""], ["Bun", "Byun", ""], ["", "", ""], ["", "", ""]]'>
</pl-drag-and-drop-grid>

</pl-question-panel>

</body>
</html>
6 changes: 6 additions & 0 deletions questions/pl-drag-and-drop-examples/drag_and_drop/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import random
import json
import prairielearn as pl

def generate(data):
pass
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions questions/pl-drag-and-drop-examples/robot/info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"uuid": "a93eb347-bdff-420f-93ed-4c92f1bed8ba",
"title": "Drag and Drop",
"topic": "Template",
"tags": [
"mwest",
"fa17"
],
"type": "v3"
}
Loading
Loading