Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Search-driven ballot (large ballots) #390

Merged
merged 62 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
918c493
wip
Findeton Sep 5, 2023
2fd2d34
wip
Findeton Sep 5, 2023
5f0e03e
wip
Findeton Sep 5, 2023
913b374
wip
Findeton Sep 5, 2023
a459282
wip
Findeton Sep 5, 2023
27d91a2
wip
Findeton Sep 5, 2023
7b6a62f
wip
Findeton Sep 5, 2023
37a45f8
wip
Findeton Sep 5, 2023
0a68296
wip
Findeton Sep 6, 2023
b10d586
wip
Findeton Sep 6, 2023
ecf2ca0
wip
Findeton Sep 6, 2023
bb66f23
wip
Findeton Sep 6, 2023
85b34fc
wip
Findeton Sep 7, 2023
64cbb4d
wip
Findeton Sep 7, 2023
451411c
wip
Findeton Sep 7, 2023
feecc41
wip
Findeton Sep 7, 2023
c5d07e4
wip
Findeton Sep 7, 2023
637bb41
wip
Findeton Sep 7, 2023
fc2ee44
wip
Findeton Sep 7, 2023
64fa948
wip
Findeton Sep 7, 2023
d8f3191
wip
Findeton Sep 7, 2023
3f8851b
wip
Findeton Sep 7, 2023
63577f2
wip
Findeton Sep 7, 2023
6fe8e99
wip
Findeton Sep 7, 2023
29a6f80
wip
Findeton Sep 7, 2023
5583f4d
wip
Findeton Sep 7, 2023
0ab33fa
wip
Findeton Sep 7, 2023
32a8b85
wip
Findeton Sep 7, 2023
845e106
wip
Findeton Sep 7, 2023
e341d2a
wip
Findeton Sep 7, 2023
85163df
wip
Findeton Sep 7, 2023
354285d
wip
Findeton Sep 7, 2023
2343d50
wip
Findeton Sep 7, 2023
e03d55d
wip
Findeton Sep 7, 2023
0aca586
wip
Findeton Sep 7, 2023
c639922
wip
Findeton Sep 7, 2023
16e5317
wip
Findeton Sep 7, 2023
17b0aca
wip
Findeton Sep 7, 2023
ebcf729
wip
Findeton Sep 7, 2023
f56a0a5
wip
Findeton Sep 7, 2023
a1a98eb
wip
Findeton Sep 7, 2023
6d22561
wip
Findeton Sep 7, 2023
f949aef
wip
Findeton Sep 7, 2023
5612a44
wip
Findeton Sep 7, 2023
6a593bf
wip
Findeton Sep 7, 2023
4ae77f0
wip
Findeton Sep 7, 2023
93f6f60
wip
Findeton Sep 7, 2023
cc9b0b0
wip
Findeton Sep 8, 2023
a868ad5
wip
Findeton Sep 8, 2023
1660e7f
wip
Findeton Sep 8, 2023
4da269f
wip
Findeton Sep 8, 2023
807b18e
wip
Findeton Sep 8, 2023
9e82bad
wip
Findeton Sep 8, 2023
0141b2c
wip
Findeton Sep 8, 2023
96b757b
wip
Findeton Sep 8, 2023
500bd48
wip
Findeton Sep 8, 2023
ea10342
wip
Findeton Sep 8, 2023
7c325e3
wip
Findeton Sep 8, 2023
f4cb2cb
wip
Findeton Sep 8, 2023
b7e7a05
wip
Findeton Sep 8, 2023
873a11b
wip
Findeton Sep 8, 2023
9ebf4c6
wip
Findeton Sep 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading