From 137eddcdb4c01ec637dc646f7261f5d64f484b95 Mon Sep 17 00:00:00 2001 From: Noe Antoine Date: Wed, 14 Jul 2021 13:09:51 +0000 Subject: [PATCH] [IMP] survey: improve image readability and add zoom widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- addons/survey/__manifest__.py | 1 + addons/survey/models/survey_question.py | 3 +- addons/survey/static/src/js/survey_form.js | 23 ++++ .../static/src/js/survey_image_zoomer.js | 112 ++++++++++++++++++ .../src/scss/survey_templates_form.scss | 63 ++++++++++ .../src/xml/survey_image_zoomer_templates.xml | 27 +++++ addons/survey/views/survey_question_views.xml | 3 +- addons/survey/views/survey_templates.xml | 4 +- 8 files changed, 232 insertions(+), 4 deletions(-) create mode 100644 addons/survey/static/src/js/survey_image_zoomer.js create mode 100644 addons/survey/static/src/xml/survey_image_zoomer_templates.xml diff --git a/addons/survey/__manifest__.py b/addons/survey/__manifest__.py index cb8f221dd49..abadb891fd5 100644 --- a/addons/survey/__manifest__.py +++ b/addons/survey/__manifest__.py @@ -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', diff --git a/addons/survey/models/survey_question.py b/addons/survey/models/survey_question.py index 0d74c108285..bf91546bcea 100644 --- a/addons/survey/models/survey_question.py +++ b/addons/survey/models/survey_question.py @@ -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") diff --git a/addons/survey/static/src/js/survey_form.js b/addons/survey/static/src/js/survey_form.js index dd44314d636..ef8dcbfb235 100644 --- a/addons/survey/static/src/js/survey_form.js +++ b/addons/survey/static/src/js/survey_form.js @@ -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). diff --git a/addons/survey/static/src/js/survey_image_zoomer.js b/addons/survey/static/src/js/survey_image_zoomer.js new file mode 100644 index 00000000000..5a7b446a7ac --- /dev/null +++ b/addons/survey/static/src/js/survey_image_zoomer.js @@ -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; + }, +}); diff --git a/addons/survey/static/src/scss/survey_templates_form.scss b/addons/survey/static/src/scss/survey_templates_form.scss index 42919a4b225..dadcb408992 100644 --- a/addons/survey/static/src/scss/survey_templates_form.scss +++ b/addons/survey/static/src/scss/survey_templates_form.scss @@ -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; + } + } diff --git a/addons/survey/static/src/xml/survey_image_zoomer_templates.xml b/addons/survey/static/src/xml/survey_image_zoomer_templates.xml new file mode 100644 index 00000000000..d01c6873934 --- /dev/null +++ b/addons/survey/static/src/xml/survey_image_zoomer_templates.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/addons/survey/views/survey_question_views.xml b/addons/survey/views/survey_question_views.xml index 3b67805f217..9a78a1e2fee 100644 --- a/addons/survey/views/survey_question_views.xml +++ b/addons/survey/views/survey_question_views.xml @@ -145,7 +145,8 @@ attrs="{'column_invisible': ['|', ('parent.scoring_type', '=', 'no_scoring'), ('parent.question_type', '=', 'matrix')]}"/> - + diff --git a/addons/survey/views/survey_templates.xml b/addons/survey/views/survey_templates.xml index c235564d94f..be6de09bce4 100644 --- a/addons/survey/views/survey_templates.xml +++ b/addons/survey/views/survey_templates.xml @@ -402,11 +402,11 @@
+ class="o_survey_choice_img d-flex my-3 justify-content-center">