Skip to content

Commit

Permalink
✨ Search-driven ballot (large ballots) (#390)
Browse files Browse the repository at this point in the history
Parent issue: sequentech/meta#126
  • Loading branch information
Findeton committed Sep 20, 2023
1 parent cb30be6 commit a2b4280
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 25 deletions.
5 changes: 1 addition & 4 deletions avBooth/booth-directive/booth-directive.less
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,7 @@
gap: 8px;
border-radius: 4px;
line-height: 19px;
}

.glyphicon-warning-sign {
margin-top: 3px;
align-items: center;
}
}

Expand Down
75 changes: 75 additions & 0 deletions avBooth/search-filter-service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* This file is part of voting-booth.
* Copyright (C) 2023 Sequent Tech Inc <legal@sequentech.io>
* voting-booth is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License.
* voting-booth is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public License
* along with voting-booth. If not, see <http://www.gnu.org/licenses/>.
**/

angular.module('avBooth')
.factory('SearchFilter', function($filter) {
var service = {};

service.isStringContained = function (searchTerm, text) {
// convert to lower case to make comparison case-insensitive
searchTerm = searchTerm.toLocaleLowerCase('en-US');
text = text.toLocaleLowerCase('en-US');
// transform accented letters into their decomposed form
searchTerm = searchTerm.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
// match and remove all accents
text = text.normalize("NFD").replace(/[\u0300-\u036f]/g, "");

// split the search by '*'
var searchParts = searchTerm.split("*");
// remove empty strings from multiple * in a row or initial/end *
searchParts = searchParts.filter(function (part) { return part.length > 0; });

// Loop through each part
var lastIdx = 0;
for (var part of searchParts) {
// Find the part in the text, starting from the last index found
var idx = text.indexOf(part, lastIdx);

// If the part is not found, or if it's found before the last part, return false
if (idx < 0) {
return false;
}

// Update the last index found to after the current part
lastIdx = idx + part.length;
}

// If we get through the loop without returning false, then the search term is contained in the text
return true;
};

service.isSelectedAnswer = function (searchText, answer) {
var filter = searchText.trim().toLowerCase();

// doesn't apply if there's no filter
if (0 === filter.length) {
return true;
}

var inputs = [
($filter('customI18n')(answer, 'text') || "").toLowerCase(),
($filter('customI18n')(answer, 'details') || "").toLowerCase(),
($filter('customI18n')(answer, 'category') || "").toLowerCase()
];
return inputs.map(function (text) {
return service.isStringContained(filter, text);
}).some(function (val) { return val; });

};

return service;
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ <h3 class="category-heading">
class="question-answers text-left"
>
<div
ng-if="!isReview"
ng-if="!isReview && (category.isCategorySelected || answer.isFilterSelected)"
ng-repeat="answer in category.answers"
class="question-answer-wrapper"
ng-class="{
'flex-col-12': (question.extra_options.answer_columns_size === 12),
'flex-col-6': (question.extra_options.answer_columns_size === 6),
'flex-col-4': (question.extra_options.answer_columns_size === 4),
'flex-col-3': (question.extra_options.answer_columns_size === 3)
'flex-col-12': ((question.search && question.isAnyCategorySelected) || question.extra_options.answer_columns_size === 12),
'flex-col-6': ((!question.search || !question.isAnyCategorySelected) && question.extra_options.answer_columns_size === 6),
'flex-col-4': ((!question.search || !question.isAnyCategorySelected) && question.extra_options.answer_columns_size === 4),
'flex-col-3': ((!question.search || !question.isAnyCategorySelected) && question.extra_options.answer_columns_size === 3)
}"
>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,47 @@ <h2 class="question-title" aria-level="2" ng-bind-html="question | customI18n :
ng-if="question.description || question.description_i18n"
ng-bind-html="question | customI18n : 'description' | addTargetBlank">
</p>

<div class="question-search-container">
<div
class="filter-input-wrapper"
ng-if="question.showSearch"
>
<input type="text"
class="filter-input"
ng-attr-id="filter-input-{{$index}}"
ng-model="question.search"
ng-model-options="{debounce: 500}"
ng-i18next="[placeholder]avBooth.simultaneousQuestions.filterOptionsPlaceholder;[aria-label]avBooth.simultaneousQuestions.filterOptionsPlaceholder"
/>
<span
class="glyphicon glyphicon-search"
ng-attr-id="search-icon-{{$index}}"
></span>
<i
class="fa fa-times"
ng-class="{'hide': !question.search}"
ng-click="clearSearch(question)"
></i>
</div>
</div>
<div class="input-warn warn-blue">
<div class="warn-box" ng-if="question.showSearch && question.search">
<span class="glyphicon glyphicon-warning-sign"></span>
<div
class="warn-text"
role="alert"
ng-i18next="avBooth.simultaneousQuestions.searchWarn">
</div>
</div>
</div>

<!-- QUESTION OPTIONS START -->

<div
class="invalid-vote-col"
ng-class="{'extra-invalid-vote-space': question.hasCategories}"
ng-if="!!question.invalidVoteAnswer && 'top' === question.invalidVoteAnswer.position"
ng-if="!!question.invalidVoteAnswer && 'top' === question.invalidVoteAnswer.position && question.invalidVoteAnswer.isFilterSelected"
>
<div
avb-simultaneous-question-answer-v2
Expand All @@ -73,16 +107,17 @@ <h2 class="question-title" aria-level="2" ng-bind-html="question | customI18n :
avb-simultaneous-questions-category-v2
question="question"
category="category"
ng-if="category.isCategorySelected || category.isAnyAnswerSelected"
toggle-select-item="toggleSelectItem"
toggle-select-item-cumulative="toggleSelectItemCumulative"
cumulative-checks="cumulativeChecks"
write-in-text-change="updateErrors"
is-review="false"
ng-class="{
'flex-col-12': (question.extra_options.answer_group_columns_size === 12),
'flex-col-6': (question.extra_options.answer_group_columns_size === 6),
'flex-col-4': (question.extra_options.answer_group_columns_size === 4),
'flex-col-3': (question.extra_options.answer_group_columns_size === 3),
'flex-col-12': ((question.search && question.isAnyCategorySelected) || question.extra_options.answer_group_columns_size === 12),
'flex-col-6': ((!question.search || !question.isAnyCategorySelected) && question.extra_options.answer_group_columns_size === 6),
'flex-col-4': ((!question.search || !question.isAnyCategorySelected) && question.extra_options.answer_group_columns_size === 4),
'flex-col-3': ((!question.search || !question.isAnyCategorySelected) && question.extra_options.answer_group_columns_size === 3),
'empty-category': !category.title
}"
>
Expand All @@ -92,13 +127,13 @@ <h2 class="question-title" aria-level="2" ng-bind-html="question | customI18n :
<div class="question-answers text-left">
<div
ng-repeat="answer in question.answers"
ng-if="!isInvalidAnswer(answer)"
ng-if="!isInvalidAnswer(answer) && answer.isFilterSelected"
class="question-answer-wrapper"
ng-class="{
'flex-col-12': (question.extra_options.answer_columns_size === 12),
'flex-col-6': (question.extra_options.answer_columns_size === 6),
'flex-col-4': (question.extra_options.answer_columns_size === 4),
'flex-col-3': (question.extra_options.answer_columns_size === 3)
'flex-col-12': ((question.search && question.isAnyCategorySelected) || question.extra_options.answer_columns_size === 12),
'flex-col-6': ((!question.search || !question.isAnyCategorySelected) && question.extra_options.answer_columns_size === 6),
'flex-col-4': ((!question.search || !question.isAnyCategorySelected) && question.extra_options.answer_columns_size === 4),
'flex-col-3': ((!question.search || !question.isAnyCategorySelected) && question.extra_options.answer_columns_size === 3)
}"
>
<div
Expand All @@ -119,7 +154,7 @@ <h2 class="question-title" aria-level="2" ng-bind-html="question | customI18n :
<div
class="invalid-vote-col"
ng-class="{'extra-invalid-vote-space': question.hasCategories}"
ng-if="!!question.invalidVoteAnswer && 'bottom' === question.invalidVoteAnswer.position"
ng-if="!!question.invalidVoteAnswer && 'bottom' === question.invalidVoteAnswer.position && question.invalidVoteAnswer.isFilterSelected"
>
<div
avb-simultaneous-question-answer-v2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ angular.module('avBooth')
$window,
ConfigService,
CheckerService,
ErrorCheckerGeneratorService
ErrorCheckerGeneratorService,
SearchFilter
) {
var simultaneousQuestionsLayouts = ["simultaneous-questions-v2", "simultaneous-questions"];

Expand Down Expand Up @@ -319,6 +320,45 @@ angular.module('avBooth')
};

scope.groupQuestions = groupQuestions;

scope.clearSearch = function (question) {
question.search = "";
};

function updateFilteredAnswers(question) {
return function() {
for (var answer of question.answers) {
answer.isFilterSelected = SearchFilter.isSelectedAnswer(question.search, answer);
}

question.isAnyCategorySelected = false;
if (question.hasCategories && !!question.categories) {
for (var category of question.categories) {
category.isCategorySelected =
SearchFilter.isStringContained(question.search, category.title);
if (category.categoryAnswer) {
category.isCategorySelected =
SearchFilter.isSelectedAnswer(question.search, category.categoryAnswer);
}
question.isAnyCategorySelected = question.isAnyCategorySelected || category.isCategorySelected;

var isAnyAnswerSelected = false;
for (var catAnswer of category.answers) {
catAnswer.isFilterSelected =
SearchFilter.isSelectedAnswer(question.search, catAnswer);
isAnyAnswerSelected = isAnyAnswerSelected || catAnswer.isFilterSelected;
}
category.isAnyAnswerSelected = isAnyAnswerSelected;
}
}
};
}
scope.groupQuestions.forEach(function (question, index) {
question.search = "";
question.showSearch = question.extra_options && question.extra_options.show_filter_field;
scope.$watch("groupQuestions[" + index + "].search", updateFilteredAnswers(question));
});

var lastGroupQuestionArrayIndex = groupQuestions[groupQuestions.length-1];
var lastGroupQuestionIndex = lastGroupQuestionArrayIndex.num;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,59 @@
}
}
}

.filter-input-wrapper {
position: relative;

.filter-input {
font-size: 14px;
line-height: 24px;
height: 36px;
padding-left: 41px;
border-radius: 4px;
border: 2px solid rgba(0, 0, 0, 0.05);
width: 237px;

@media(max-width: @screen-xs-max) {
width: 183px;
}
}

.glyphicon.glyphicon-search {
position: absolute;
left: 15px;
font-size: 18px;
top: 9px;
color: #64748B;
}

.fa.fa-times {
position: absolute;
right: 12px;
font-size: 18px;
top: 9px;
color: #64748B;
cursor: pointer;
}

.hide {
display: none;
}
}

.question-search-container {
width: 100%;
display: flex;
flex-direction: row;
justify-content: flex-end;
margin-bottom: 12px;
}

.input-warn.warn-blue {
.warn-box {
background-color: #CCE5FF;
color: #292F99;
margin-bottom: 16px;
}
}
}
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
<script src="app.js" class="app"></script>
<script src="avBooth/booth.js" class="app"></script>
<script src="avBooth/is-service.js" class="app"></script>
<script src="avBooth/search-filter-service.js" class="app"></script>
<script src="avBooth/ahoram-primaries-screen-directive/ahoram-primaries-screen-directive.js" class="app"></script>
<script src="avBooth/ahoram-primaries-option-directive/ahoram-primaries-option-directive.js" class="app"></script>
<script src="avBooth/ahoram-primaries-selected-option-directive/ahoram-primaries-selected-option-directive.js" class="app"></script>
Expand Down
4 changes: 3 additions & 1 deletion locales/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,9 @@
"header": "Informació: Pantalla de votació",
"body": "Aquesta pantalla mostra la votació en la quals ets elegible per votar. Pots seleccionar la teva secció activant la casella de la dreta Candidat/Resposta. Per a restablir les teves seleccions, fes clic al botó '<b>Esborrar selecció</b>', per passar al següent pas, fes clic al botó '<b>Següent</b>'.",
"confirm": "D'acord"
}
},
"searchWarn": "Els candidats de la pregunta han estat filtrats. Per veure tots els candidats en aquesta pregunta, si us plau desactiva el filtre.",
"filterOptionsPlaceholder": "Cerca un Candidat"
},

"pairwiseBeta": {
Expand Down
4 changes: 3 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,9 @@
"header": "Information: Ballot screen",
"body": "This screen shows the contest you are elegible to vote. You can make your section by activate the checkbox on the Candidate/Answer right. To reset your selections, click “<b>Clear selection</b>” button, to move to next step, click “<b>Next</b>” button bellow.",
"confirm": "OK"
}
},
"searchWarn": "Ballot candidates have been filtered. To view all candidates on this ballot, please disable the filter.",
"filterOptionsPlaceholder": "Search a Candidate"
},

"pairwiseBeta": {
Expand Down
4 changes: 3 additions & 1 deletion locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,9 @@
"header": "Información: Pantalla de votación",
"body": "Esta pantalla muestra la votación en la que usted es elegible para votar. Puede seleccionar su sección activando la casilla de la derecha Candidato/Respuesta. Para restablecer sus selecciones, haga clic en el botón “<b>Borrar selección</b>”, para pasar al siguiente paso, haga clic en el botón “<b>Siguiente</b>”.",
"confirm": "OK"
}
},
"searchWarn": "Los candidatos de la pregunta han sido filtrados. Para ver a todos los candidatos en esta pregunta, por favor desactiva el filtro.",
"filterOptionsPlaceholder": "Busca un Candidato"
},

"pairwiseBeta": {
Expand Down
4 changes: 3 additions & 1 deletion locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,9 @@
"header": "Voulez-vous sauter cette ou ces questions ?",
"body": "Veuillez confirmer que vous voulez sauter cette (ces) question(s). Vous pouvez voter pour la ou les questions ignor\u00e9es en vous connectant \u00e0 nouveau.",
"confirm": "Confirmer et SAUTER cette(ces) question(s)"
}
},
"searchWarn": "Les candidats de la question ont été filtrés. Pour voir tous les candidats de cette question, veuillez désactiver le filtre.",
"filterOptionsPlaceholder": "Cherchez un Candidat"
},
"pairwiseBeta": {
"web": "Web",
Expand Down

0 comments on commit a2b4280

Please sign in to comment.