diff --git a/survey_dropdown_choice/__init__.py b/survey_dropdown_choice/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/survey_dropdown_choice/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/survey_dropdown_choice/__manifest__.py b/survey_dropdown_choice/__manifest__.py new file mode 100644 index 0000000..0be6136 --- /dev/null +++ b/survey_dropdown_choice/__manifest__.py @@ -0,0 +1,29 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "Survey dropdown choice", + "summary": "Display a 'simple choice' question as a searchable dropdown instead of radio buttons", + "description": """ +Adds an option on 'Multiple choice: only one answer' questions to render the +suggested answers as a searchable dropdown. While taking the survey, the +respondent can type in the input to filter the visible options, which is much +more convenient than scrolling through a long radio-button list. +""", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "author": "Elabore", + "website": "https://www.elabore.coop", + "category": "Marketing/Surveys", + "depends": ["survey"], + "data": [ + "views/survey_question_views.xml", + "views/survey_templates.xml", + ], + "assets": { + "survey.survey_assets": [ + "survey_dropdown_choice/static/src/scss/survey_dropdown_choice.scss", + "survey_dropdown_choice/static/src/js/survey_dropdown_choice.js", + ], + }, + "installable": True, +} diff --git a/survey_dropdown_choice/models/__init__.py b/survey_dropdown_choice/models/__init__.py new file mode 100644 index 0000000..09074bf --- /dev/null +++ b/survey_dropdown_choice/models/__init__.py @@ -0,0 +1 @@ +from . import survey_question diff --git a/survey_dropdown_choice/models/survey_question.py b/survey_dropdown_choice/models/survey_question.py new file mode 100644 index 0000000..9cf814b --- /dev/null +++ b/survey_dropdown_choice/models/survey_question.py @@ -0,0 +1,13 @@ +from odoo import fields, models + + +class SurveyQuestion(models.Model): + _inherit = "survey.question" + + display_dropdown = fields.Boolean( + string="Display as searchable dropdown", + help="Render the suggested answers as a searchable dropdown instead of " + "a list of radio buttons. Only relevant for 'Multiple choice: only " + "one answer' questions. The respondent can type in the dropdown to " + "filter the available options.", + ) diff --git a/survey_dropdown_choice/static/src/js/survey_dropdown_choice.js b/survey_dropdown_choice/static/src/js/survey_dropdown_choice.js new file mode 100644 index 0000000..5cad0ea --- /dev/null +++ b/survey_dropdown_choice/static/src/js/survey_dropdown_choice.js @@ -0,0 +1,185 @@ +/** @odoo-module **/ + +import publicWidget from "@web/legacy/js/public/public_widget"; + +// Strip combining diacritical marks (U+0300..U+036F) so "Élève" matches "eleve". +const STRIP_DIACRITICS = (str) => + (str || "").normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase(); + +publicWidget.registry.SurveyDropdownChoice = publicWidget.Widget.extend({ + selector: ".o_survey_dropdown_choice", + events: { + "input .o_survey_dropdown_search": "_onSearchInput", + "focus .o_survey_dropdown_search": "_onSearchFocus", + "click .o_survey_dropdown_search_wrapper": "_onWrapperClick", + "keydown .o_survey_dropdown_search": "_onSearchKeydown", + "mousedown .o_survey_dropdown_option": "_onOptionMousedown", + "click .o_survey_dropdown_option": "_onOptionClick", + "click input[type='radio']": "_onRadioInputClick", + }, + + /** + * @override + */ + start() { + this._panelEl = this.el.querySelector(".o_survey_dropdown_panel"); + this._searchEl = this.el.querySelector(".o_survey_dropdown_search"); + this._noMatchEl = this.el.querySelector(".o_survey_dropdown_no_match"); + this._options = Array.from(this.el.querySelectorAll(".o_survey_dropdown_option")); + this._onDocumentMousedown = this._onDocumentMousedown.bind(this); + document.addEventListener("mousedown", this._onDocumentMousedown); + return this._super(...arguments); + }, + + /** + * @override + */ + destroy() { + document.removeEventListener("mousedown", this._onDocumentMousedown); + return this._super(...arguments); + }, + + // -------------------------------------------------------------------------- + // Handlers + // -------------------------------------------------------------------------- + + _onWrapperClick() { + // Make sure clicking anywhere in the search row (chevron, padding, ...) + // focuses the input, which in turn opens the panel. + if (document.activeElement !== this._searchEl) { + this._searchEl.focus(); + } else { + this._openPanel(); + this._filter(""); + } + }, + + _onSearchFocus() { + this._openPanel(); + this._filter(""); + }, + + _onSearchInput(ev) { + this._openPanel(); + this._filter(ev.currentTarget.value); + }, + + _onSearchKeydown(ev) { + if (ev.key === "Escape") { + this._closePanel(); + ev.currentTarget.blur(); + } + }, + + /** + * Prevent the search input's `blur` from firing before `click` resolves + * (otherwise the panel would close and swallow the click). + */ + _onOptionMousedown(ev) { + ev.preventDefault(); + }, + + /** + * The wrapping