[IMP] survey: improve image readability and add zoom widget

Before this commit, images displayed on survey question answers
were small and hard to read, and nothing existed in order to zoom
on them properly.

AFTER THIS COMMIT

Backend:

A new field value_image_filename is added to store the names of
the image fields. They are seen when editing an answer (line).

The max_height and max_width of the images are raised to 1024 px
on their field definition in survey.question.answer to compromise
between image resolution and space, and in order to be able to
zoom on them and still see details

Survey:

A hovering frame is visible on the images, as well as a zoom in
cursor, to indicate we can click on them to zoom in. The rest of
the answer frame is the selection area. Clicking anywhere on the
modal closes it. Until then, a pointer cursor hints the feature.

A new widget SurveyImageZoomer is added. Once the user has clicked
on a choice image, a transparent modal is opened on the whole screen.
This is thought for all size devices : it can be closed by clicking
anywhere, or the cross button, or ESC key. It will always be destroyed.
Also, for small width screens, we do not take the whole height but leave
bands over and below to indicate the mobile users can leave by clicking out.

The image will be displayed in the foreground, zooming on small images
by default, by setting a min width depending on the screen width. We
also avoid using a zoom too large to avoid very low resolution.

The widget allows zooming as long as the image stays in the frame.
The user can zoom both with the mouse scrolling and + and - buttons,
sized to allow medium screen (tablets) to press them accurately.

All keys are disabled but the 'ESC' one when the Zoomer is open to
avoid (undesired) survey navigation.

Task-2566584

closes odoo/odoo#73718

Signed-off-by: Warnon Aurélien (awa) <awa@odoo.com>
This commit is contained in:
Noe Antoine
2022-08-03 13:44:46 +02:00
parent 64b6b630b9
commit 137eddcdb4
8 changed files with 232 additions and 4 deletions
+1
View File
@@ -61,6 +61,7 @@ sent mails with personal token for the invitation of the survey.
'assets': {
'survey.survey_assets': [
'web/static/lib/Chart/Chart.js',
'survey/static/src/js/survey_image_zoomer.js',
'survey/static/src/js/survey_quick_access.js',
'survey/static/src/js/survey_timer.js',
'survey/static/src/js/survey_breadcrumb.js',
+2 -1
View File
@@ -600,7 +600,8 @@ class SurveyQuestionAnswer(models.Model):
scoring_type = fields.Selection(related='question_id.scoring_type')
# answer related fields
value = fields.Char('Suggested value', translate=True, required=True)
value_image = fields.Image('Image', max_width=256, max_height=256)
value_image = fields.Image('Image', max_width=1024, max_height=1024)
value_image_filename = fields.Char('Image Filename')
is_correct = fields.Boolean('Correct')
answer_score = fields.Float('Score', help="A positive score indicates a correct choice; a negative or null score indicates a wrong answer")
@@ -11,6 +11,7 @@ var dom = require('web.dom');
var utils = require('web.utils');
var SurveyPreloadImageMixin = require('survey.preload_image_mixin');
const { SurveyImageZoomer } = require("@survey/js/survey_image_zoomer");
var _t = core._t;
var isMac = navigator.platform.toUpperCase().includes('MAC');
@@ -22,6 +23,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
'click .o_survey_matrix_btn': '_onMatrixBtnClick',
'click input[type="radio"]': '_onRadioChoiceClick',
'click button[type="submit"]': '_onSubmit',
'click .o_survey_choice_img img': '_onChoiceImgClick',
'focusin .form-control': '_updateEnterButtonText',
'focusout .form-control': '_updateEnterButtonText'
},
@@ -43,6 +45,7 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
self.options = self.$target.find('form').data();
self.readonly = self.options.readonly;
self.selectedAnswers = self.options.selectedAnswers;
self.imgZoomer = false;
// Add Survey cookie to retrieve the survey if you quit the page and restart the survey.
if (!utils.get_cookie('survey_' + self.options.surveyToken)) {
@@ -108,6 +111,10 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
if (this.$('fieldset[disabled="disabled"]').length !== 0) {
return;
}
// Disable all navigation keys when zoom modal is open, except the ESC.
if ((this.imgZoomer && !this.imgZoomer.isDestroyed()) && keyCode !== 27) {
return;
}
var letter = String.fromCharCode(keyCode).toUpperCase();
@@ -255,6 +262,22 @@ publicWidget.registry.SurveyFormWidget = publicWidget.Widget.extend(SurveyPreloa
}
},
/**
* Called when an image on an answer in multi-answers question is clicked.
* Starts a widget opening a dialog to display the now zoomable image.
* this.imgZoomer is the zoomer widget linked to the survey form, if any.
*
* @private
* @param {Event} ev
*/
_onChoiceImgClick: function (ev) {
ev.preventDefault();
this.imgZoomer = new SurveyImageZoomer({
sourceImage: $(ev.currentTarget).attr('src')
});
this.imgZoomer.appendTo(document.body);
},
/**
* Invert the related input's "checked" property.
* This will tick or untick the option (based on the previous state).
@@ -0,0 +1,112 @@
/** @odoo-module */
import publicWidget from 'web.public.widget';
export const SurveyImageZoomer = publicWidget.Widget.extend({
template: 'survey.survey_image_zoomer',
xmlDependencies: ['/survey/static/src/xml/survey_image_zoomer_templates.xml'],
events: {
'wheel .o_survey_img_zoom_image': '_onImgScroll',
'click': '_onZoomerClick',
'click .o_survey_img_zoom_in_btn': '_onZoomInClick',
'click .o_survey_img_zoom_out_btn': '_onZoomOutClick',
},
/**
* @override
*/
init(params) {
this.zoomImageScale = 1;
// The image is needed to render the template survey_image_zoom.
this.sourceImage = params.sourceImage;
this._super(... arguments);
},
/**
* Open a transparent modal displaying the survey choice image.
* @override
*/
async start() {
const superResult = await this._super(...arguments);
// Prevent having hidden modal in the view.
this.$el.on('hidden.bs.modal', () => this.destroy());
this.$el.modal('show');
return superResult;
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Zoom in/out image on scrolling
*
* @private
* @param {WheelEvent} e
*/
_onImgScroll(e) {
e.preventDefault();
if (e.originalEvent.wheelDelta > 0 || e.originalEvent.detail < 0) {
this._addZoomSteps(1);
} else {
this._addZoomSteps(-1);
}
},
/**
* Allow user to close by clicking anywhere (mobile...). Destroying the modal
* without using 'hide' would leave a modal-open in the view.
* @private
* @param {Event} e
*/
_onZoomerClick(e) {
e.preventDefault();
this.$el.modal('hide');
},
/**
* @private
* @param {Event} e
*/
_onZoomInClick(e) {
e.stopPropagation();
this._addZoomSteps(1);
},
/**
* @private
* @param {Event} e
*/
_onZoomOutClick(e) {
e.stopPropagation();
this._addZoomSteps(-1);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Zoom in / out the image by changing the scale by the given number of steps.
*
* @private
* @param {integer} zoomStepNumber - Number of zoom steps applied to the scale of
* the image. It can be negative, in order to zoom out. Step is set to 0.1.
*/
_addZoomSteps(zoomStepNumber) {
const image = this.el.querySelector('.o_survey_img_zoom_image');
const body = this.el.querySelector('.o_survey_img_zoom_body');
const imageWidth = image.clientWidth;
const imageHeight = image.clientHeight;
const bodyWidth = body.clientWidth;
const bodyHeight = body.clientHeight;
const newZoomImageScale = this.zoomImageScale + zoomStepNumber * 0.1;
if (newZoomImageScale <= 0.2) {
// Prevent the user from de-zooming too much
return;
}
if (zoomStepNumber > 0 && (imageWidth * newZoomImageScale > bodyWidth || imageHeight * newZoomImageScale > bodyHeight)) {
// Prevent to user to further zoom in as the new image would becomes too large or too high for the screen.
// Dezooming is still allowed to bring back image into frame (use case: resizing screen).
return;
}
// !important is needed to prevent default 'no-transform' on smaller screens.
image.setAttribute('style', 'transform: scale(' + newZoomImageScale + ') !important');
this.zoomImageScale = newZoomImageScale;
},
});
@@ -253,6 +253,16 @@ _::-webkit-full-page-media, _:future, :root .o_survey_wrap {
}
}
.o_survey_choice_img img {
max-width: 95%;
max-height: 60vh;
cursor: zoom-in;
&:hover {
box-sizing: border-box;
box-shadow: 0 0 5px 2px grey;
}
}
.o_survey_choice_key {
width: 25px;
height: 25px;
@@ -473,4 +483,57 @@ _::-webkit-full-page-media, _:future, :root .o_survey_wrap {
.o_survey_choice_question_skipped {
color: darken($warning, 10%);
}
.o_survey_choice_img img {
cursor: default;
&:hover {
box-shadow: none;
}
}
}
/**********************************************************
Zoomer Specific Style (SurveyImageZoomer widget)
When the width is small (mobile), let space above and below
to indicate that the user can close it by clicking out.
**********************************************************/
.o_survey_img_zoom_modal {
cursor: pointer;
.o_survey_img_zoom_dialog {
background-color: rgba(0,0,0,0.65);
@include media-breakpoint-down(sm) {
height: 80% !important;
}
.o_survey_img_zoom_body {
font-size: 1.5rem;
img {
max-width: 90%;
min-width: clamp(250px, 60%, 450px);
max-height: 90%;
object-fit: contain;
}
.o_survey_img_zoom_close_btn {
right: 12px;
top: 12px;
z-index: 1;
}
.o_survey_img_zoom_controls_wrapper {
bottom: 5%;
.o_survey_img_zoom_in_btn, .o_survey_img_zoom_out_btn {
background-color: rgba(0,0,0,0.65);
&:hover .fa {
color: grey;
}
}
}
}
}
}
// Avoid shifting (due to scroll bar) when opening the image zoom widget
.modal-open {
.o_survey_background {
overflow: auto !important;
}
}
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="survey.survey_image_zoomer">
<div role="dialog" class="o_survey_img_zoom_modal modal fade d-flex align-items-center p-0"
data-bs-backdrop="false" aria-labbelledby="Image Zoom Dialog" tabindex="-1">
<div class="o_survey_img_zoom_dialog modal-dialog h-100 w-100 mw-100 py-0" role="Picture Enlarged">
<div class="modal-content h-100 bg-transparent">
<div class="o_survey_img_zoom_body modal-body h-100 bg-transparent d-flex justify-content-center">
<button type="button" data-bs-dismiss="modal" aria-label="close"
class="o_survey_img_zoom_close_btn close text-white btn-close-white btn-close position-absolute"/>
<img class="o_survey_img_zoom_image img img-fluid d-block m-auto" t-att-src="widget.sourceImage" alt="Zoomed Image"/>
<div class="o_survey_img_zoom_controls_wrapper position-absolute">
<button type="button" class="o_survey_img_zoom_in_btn text-white me-1" aria-label="Zoom in">
<span class="fa fa-plus"/>
</button>
<button type="button" class="o_survey_img_zoom_out_btn text-white ms-1" aria-label="Zoom out">
<span class="fa fa-minus"/>
</button>
</div>
</div>
</div>
</div>
</div>
</t>
</templates>
@@ -145,7 +145,8 @@
attrs="{'column_invisible': ['|', ('parent.scoring_type', '=', 'no_scoring'), ('parent.question_type', '=', 'matrix')]}"/>
<field name="answer_score"
attrs="{'column_invisible': ['|', ('parent.scoring_type', '=', 'no_scoring'), ('parent.question_type', '=', 'matrix')]}"/>
<field name="value_image" options="{'accepted_file_extensions': 'image/*'}"
<field name="value_image_filename" invisible="1"/>
<field name="value_image" filename="value_image_filename" options="{'accepted_file_extensions': 'image/*'}"
attrs="{'column_invisible': [('parent.question_type', '=', 'matrix')]}"/>
</tree>
</field>
+2 -2
View File
@@ -402,11 +402,11 @@
<t t-if="label.value_image">
<!-- Directly use field or route if the user doesn't have access rights -->
<div t-if="not env.user.has_group('survey.group_survey_user')"
class="d-flex my-3 justify-content-center">
class="o_survey_choice_img d-flex my-3 justify-content-center">
<img t-att-src="'/survey/get_question_image/%s/%s/%s/%s' % (survey.access_token, answer.access_token, question.id, label.id)"/>
</div>
<div t-else="" t-field="label.value_image"
class="d-flex my-3 justify-content-center"
class="o_survey_choice_img d-flex my-3 justify-content-center"
t-options="{'widget': 'image', 'alt-field': 'name', 'itemprop': 'image'}"/>
</t>
</template>