Skip to content

Commit

Permalink
Add Zoom Functionality For Cropping Exam Template (#6640)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Anis Singh <[email protected]>
  • Loading branch information
anissingh and Anis Singh authored Jun 27, 2023
1 parent 2868692 commit 30b76d2
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 38 deletions.
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- A user can't run tests with the same names / Single test error does not spoil the whole test batch (#6620)
- Change icon set to Font Awesome (Free) (#6627)
- Add feature to generate a PDF report of a result (PDF submission files only) (#6635)
- Add zoom feature to scanned exam template crop selection (#6640)

## [v2.2.3]
- Fix bug where in some circumstances the wrong result would be displayed to students (#6465)
Expand Down
2 changes: 2 additions & 0 deletions app/assets/javascripts/fontawesome_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
faGear,
faGripVertical,
faLink,
faMinus,
faPen,
faPeopleGroup,
faPrint,
Expand Down Expand Up @@ -75,6 +76,7 @@ library.add(
faGear,
faGripVertical,
faLink,
faMinus,
faPen,
faPeopleGroup,
faPrint,
Expand Down
46 changes: 45 additions & 1 deletion app/assets/stylesheets/common/_markus.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1138,7 +1138,6 @@ nav {

.crop-target {
border: 1px solid $gridline;
max-width: 400px;
}

// Text editing and previews
Expand Down Expand Up @@ -1366,3 +1365,48 @@ canvas {
min-width: 250px;
width: 50%;
}

.exam-crop-container {
display: flex;
height: 600px;
width: 1000px;
}

#exam-crop-img-container {
background-color: $disabled-area;
flex: 1;
margin-right: 5px;
overflow-x: auto;
overflow-y: auto;
text-align: center;
}

.exam-crop-button-container {
display: flex;
flex-direction: column;
padding-left: 5px;
padding-top: 5px;
width: 40px;
}

#increase-crop-scale {
margin-bottom: 2px;
}

.crop-scale-button {
height: 40px;
margin-top: 2px;
min-width: 40px;
padding: 0;
transform: translate(-70px, 0);
width: 40px;
z-index: 800; // jcrop uses z-index of 690
}

.crop-scale-button > svg {
padding-right: 0;
}

.jcrop-centered {
display: inline-block;
}
133 changes: 111 additions & 22 deletions app/views/exam_templates/_boot.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
let crop_scale = 600;
const SCALE_CHANGE = 100;
let MIN_SIZE = 600;
let jcrop_api;

$(document).ready(function () {
window.modal_create_new = new ModalMarkus("#create_new_template");
$("#generate_exam_modal_submit").click(() => {
Expand All @@ -9,6 +14,20 @@ $(document).ready(function () {
});
});

/**
* Initialize an exam template form.
* @param id Exam template id
*/
const init_exam_template_form = id => {
const form = document.getElementById(`add_fields_exam_template_form_${id}`);
const parsing_input = form && form.elements[`${id}_exam_template_automatic_parsing`];

parsing_input.addEventListener("change", () => toggle_cover_page(id));

const crop_target = form.getElementsByClassName("crop-target")[0];
crop_target.onload = () => toggle_cover_page(id);
};

function add_template_division(target) {
var new_id = new Date().getTime();
var nested_form_path = `exam_template[template_divisions_attributes][${new_id}]`;
Expand Down Expand Up @@ -47,40 +66,100 @@ function toggle_cover_page(id) {

if (parsing_input.checked) {
$("#exam-cover-display-" + id).css("display", "flex");

const image_container_width = $("#exam-crop-img-container").width();
const image_container_width_rounded_down =
_round_down_to_nearest_hundred(image_container_width);
crop_scale = image_container_width_rounded_down;
MIN_SIZE = image_container_width_rounded_down;

attach_crop_box(id);
toggle_crop_scale_buttons(id);
} else {
$("#exam-cover-display-" + id).css("display", "none");
}
}

function attach_crop_box(id) {
var jcrop_api;
/**
* TODO: Refactor below functions so there are no hard dependencies on the crop_target having
* a certain class name, and relying on a form to grab widths and heights.
*/

/**
* Initialize a crop object and attach a crop box to an exam template with class crop-target, using
* form data to specify the dimensions of the crop box. If the form data does not exist, no crop
* selection is displayed.
* @param id Exam template identifier.
*/
function attach_crop_box(id) {
const form = document.getElementById(`add_fields_exam_template_form_${id}`);
const crop_target = form.getElementsByClassName("crop-target")[0];

$(crop_target).Jcrop(
{
onChange: pos => {
const stageHeight = parseFloat(
getComputedStyle(crop_target, null).height.replace("px", "")
);
const stageWidth = parseFloat(getComputedStyle(crop_target, null).width.replace("px", ""));
const {x, y, w, h} = pos;

form.elements[`${id}_exam_template_crop_x`].value = x / stageWidth;
form.elements[`${id}_exam_template_crop_y`].value = y / stageHeight;
form.elements[`${id}_exam_template_crop_width`].value = w / stageWidth;
form.elements[`${id}_exam_template_crop_height`].value = h / stageHeight;
},
keySupport: false,
},
function () {
jcrop_api = this;
}
);
if (jcrop_api !== undefined) {
jcrop_api.destroy();
}

jcrop_api = config_jcrop_api(crop_target, form, id);

// Set crop selection if values exist.
set_crop_selection(crop_target, form, id, jcrop_api);
}

/**
* Initialize event listeners for the crop zoom buttons on the exam template.
* @param id Exam template identifier.
*/
function toggle_crop_scale_buttons(id) {
$("#decrease-crop-scale").on("click", function () {
if (crop_scale - SCALE_CHANGE < MIN_SIZE) {
crop_scale = MIN_SIZE;
} else {
crop_scale -= SCALE_CHANGE;
}

attach_crop_box(id);
});

$("#increase-crop-scale").on("click", function () {
crop_scale += SCALE_CHANGE;
attach_crop_box(id);
});
}

/**
* Configure a Jcrop object and attach it to a target.
* @param crop_target Target to attach crop box to. Must be an exam template.
* @param form Form containing information of the crop selection for the target (exam template).
* @param id Exam template identifier.
* @returns Configured Jcrop object.
*/
function config_jcrop_api(crop_target, form, id) {
return $.Jcrop(crop_target, {
onChange: pos => {
const stageHeight = parseFloat(getComputedStyle(crop_target, null).height.replace("px", ""));
const stageWidth = parseFloat(getComputedStyle(crop_target, null).width.replace("px", ""));
const {x, y, w, h} = pos;

form.elements[`${id}_exam_template_crop_x`].value = x / stageWidth;
form.elements[`${id}_exam_template_crop_y`].value = y / stageHeight;
form.elements[`${id}_exam_template_crop_width`].value = w / stageWidth;
form.elements[`${id}_exam_template_crop_height`].value = h / stageHeight;
},
keySupport: false,
boxWidth: crop_scale,
boxHeight: crop_scale,
addClass: "jcrop-centered",
});
}

/**
* Display a crop selection onto a target image, given a selection has been saved.
* @param crop_target Target to display crop selection onto. Must be an exam template.
* @param form Form containing information of the crop selection for the target (exam template).
* @param id Exam template identifier.
* @param jcrop_api Configured Jcrop object that has been attached to crop_target already.
*/
function set_crop_selection(crop_target, form, id, jcrop_api) {
if (
form.elements[`${id}_exam_template_crop_x`].value &&
form.elements[`${id}_exam_template_crop_y`].value &&
Expand All @@ -97,3 +176,13 @@ function attach_crop_box(id) {
jcrop_api.setSelect([x, y, x + width, y + height]);
}
}

/**
* Round a number down to the nearest hundred.
* @param num Number to round down.
* @returns {number} Number rounded down to the nearest hundred.
* @private
*/
function _round_down_to_nearest_hundred(num) {
return Math.floor(num / 100) * 100;
}
32 changes: 17 additions & 15 deletions app/views/exam_templates/_student_info.html.erb
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
<%= content_for :head do %>
<%= javascript_tag nonce: true do %>
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById("add_fields_exam_template_form_<%= exam_template.id %>");
const parsing_input = form && form.elements["<%= exam_template.id %>_exam_template_automatic_parsing"];

parsing_input.addEventListener('change', () => toggle_cover_page(<%= exam_template.id %>));

const crop_target = form.getElementsByClassName("crop-target")[0];
crop_target.onload = () => toggle_cover_page(<%= exam_template.id %>);
init_exam_template_form(<%= exam_template.id %>)
})
<% end %>
<% end %>
Expand All @@ -22,14 +16,8 @@
<%= f.label :automatic_parsing, t('exam_templates.parsing.general') %>
</p>

<div id="exam-cover-display-<%= exam_template.id %>" class="<%= exam_template.automatic_parsing ? 'flex-display' : 'no-display' %>">
<div id="exam_cover-<%= exam_template.id %>">
<img src="<%= show_cover_course_exam_template_path(@current_course, exam_template) %>" class="crop-target"
alt="<%= t(:'exam_templates.parsing.cover_page_for', id: exam_template.id) %>"
loading="lazy">
</div>

<div class="table-with-add">
<div id="exam-cover-display-<%= exam_template.id %>" class="<%= exam_template.automatic_parsing ? 'flex-col' : 'no-display' %>">
<div>
<%= f.label :cover_fields, t('activerecord.attributes.exam_template.cover_fields') %>
<%= f.select :cover_fields,
options_for_select([[t('activerecord.attributes.user.id_number'), "id_number"],
Expand All @@ -40,6 +28,20 @@
<%= f.hidden_field :crop_width, value: exam_template.crop_width %>
<%= f.hidden_field :crop_height, value: exam_template.crop_height %>
</div>

<div id="exam_cover-<%= exam_template.id %>" class="exam-crop-container">
<div id="exam-crop-img-container">
<img src="<%= show_cover_course_exam_template_path(@current_course, exam_template) %>" class="crop-target"
alt="<%= t(:'exam_templates.parsing.cover_page_for', id: exam_template.id) %>"
loading="lazy">
</div>
<div class="exam-crop-button-container">
<button id="increase-crop-scale" class="crop-scale-button"
type="button" title="<%= I18n.t("results.zoom_in_image") %>"><i class="fa-solid fa-plus"></i></button>
<button id="decrease-crop-scale" class="crop-scale-button"
type="button" title="<%= I18n.t("results.zoom_out_image") %>"><i class="fa-solid fa-minus"></i></button>
</div>
</div>
</div>

<p><%= submit_tag t('exam_templates.parsing.save') %></p>
Expand Down
2 changes: 2 additions & 0 deletions app/views/exam_templates/edit.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ document.getElementById('editing_pane_menu').innerHTML =
locals: { exam_template: @exam_template }) %>";

<%= render partial: 'event_listeners' %>

init_exam_template_form(<%= @exam_template.id %>)

0 comments on commit 30b76d2

Please sign in to comment.