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

Add "whole word" search option #1229

Merged
merged 25 commits into from
Oct 28, 2021
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5f73ae8
wanted some whitespace
Oct 20, 2021
30ba506
refactor check to use regex instead
Oct 20, 2021
b344672
resolved todo, no need for a change in here I don't think.
Oct 20, 2021
f30eb21
this thing is sensitive...hopefully this naming will help a little
Oct 20, 2021
fc19ec8
refactor sequence a bit for readability (bonus: it's probably more pe…
Oct 20, 2021
f318dc2
refactored filter layout on word list page
Oct 21, 2021
0e06e09
added new "whole-word" checkbox
Oct 21, 2021
11753f3
added whole word awareness to the data service
Oct 21, 2021
2fa43d9
bound whole word option into view
Oct 21, 2021
f716a5f
added wholeWord to querystring for deeplinking
Oct 25, 2021
0b5e07a
logic to show reset will not change because of wholeWord
Oct 25, 2021
29202cd
cleared wholeWord on reset
Oct 25, 2021
62abaa3
added wholeWord to options indicator logic
Oct 25, 2021
fd6f0a3
wired up returnToList and made entry view aware of wholeWord option
Oct 25, 2021
d9e2369
refactored entry view to be consistent with list view filters
Oct 25, 2021
5da9215
removed static analysis hint
Oct 25, 2021
95d902a
removed unused code
Oct 26, 2021
2e4ff24
removed unnecessary comment
Oct 26, 2021
5a23556
removed unused code
Oct 26, 2021
188c84f
corrected console errors when adding a new entry
Oct 26, 2021
88c1892
corrected more console errors when adding a new entry
Oct 26, 2021
5b24804
simplified condition
Oct 26, 2021
6358b46
used regex case-insensitive instead of uppercasing (PR feedback)
Oct 27, 2021
563f246
sanitized regex tokens (PR feedback)
Oct 27, 2021
a7cf2f9
removed extraneous whitepsace (PR feedback)
Oct 28, 2021
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
27 changes: 18 additions & 9 deletions src/angular-app/bellows/core/offline/editor-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class EntryListModifiers {
};
sortOptions: SortOption[] = [];
sortReverse = false;
wholeWord = false;
filterBy: {
text: string;
option: FilterOption;
Expand Down Expand Up @@ -443,19 +444,27 @@ export class EditorDataService {
text.indexOf(query) === 0 && !UtilityService.isDigitsOnly(text.slice(query.length - 1, query.length + 1));
}

private entryMeetsFilterCriteria(config: any, entry: LexEntry): boolean {
// this ensures regex tokens are not interpreted as regex, e.g., a user searching for '[a-zA-Z]' should _probably_ result in no matches.
private escapeRegex(input: string) {
// taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matche
}

private entryMeetsFilterCriteria(config: any, entry: LexEntry): boolean {
if (this.entryListModifiers.filterText() !== '') {
const query = this.entryListModifiers.filterText().toUpperCase();
let matchesSearch = false;
this.walkEntry(config.entry, entry, (val, isSemanticDomain) => {
val = val.toUpperCase();
if (isSemanticDomain) {
if (this.semanticDomainsMatch(val, query)) matchesSearch = true;
} else if (val.indexOf(query) !== -1) matchesSearch = true;
const query = this.escapeRegex(this.entryListModifiers.filterText());
const queryRegex = new RegExp(this.entryListModifiers.wholeWord ? `\\b${query}\\b` : query, 'i');
let found = false;

this.walkEntry(config.entry, entry, (val, isSemanticDomain) => {
longrunningprocess marked this conversation as resolved.
Show resolved Hide resolved
if (queryRegex.test(val) || (isSemanticDomain && this.semanticDomainsMatch(val, query))) {
found = true
}
});
if (!matchesSearch) return false;

if (!found) return false;
}

if (!this.entryListModifiers.filterBy.option) return true;

const mustNotBeEmpty = this.entryListModifiers.filterType === 'isNotEmpty';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<span class="fa fa-times clear-search-button" data-ng-show="$ctrl.entryListModifiers.filterBy.text" data-ng-click="$ctrl.clearSearchText()"></span>
</div>
<button data-ng-click="$ctrl.toggleFilterOptions()" class="btn btn-sm">
<span class="options-active-icon fa fa-circle" data-ng-class="{'icon-active': $ctrl.filterSortOptionsActive()}"></span> Options
<span class="options-active-icon fa fa-circle" data-ng-class="{'icon-active': $ctrl.filterOptionsActive()}"></span> Options
<i class="fa" data-ng-class="$ctrl.show.entryListModifiers ? 'fa-angle-up': 'fa-angle-down'"></i>
</button>
</div>
Expand All @@ -45,7 +45,6 @@
<div class="form-group sortfilter-form">
<label class="font-weight-bold" for="filterEntriesFor">Filter Entries By</label>
<div class="form-inline">
<!--suppress HtmlFormInputWithoutLabel -->
<select class="custom-select sortfilter-control" data-ng-show="$ctrl.entryListModifiers.filterBy.option"
data-ng-change="$ctrl.filterAndSortEntries()" data-ng-model="$ctrl.entryListModifiers.filterType">
<option value="isEmpty">Doesn't have</option>
Expand All @@ -60,16 +59,29 @@
</div>
<div class="form-group sortfilter-form">
<label class="font-weight-bold" for="sortEntriesBy">Sort Entries By</label>
<label class="reverse-label">
<input type="checkbox" data-ng-change="$ctrl.filterAndSortEntries()" data-ng-model="$ctrl.entryListModifiers.sortReverse"> Reverse
</label>

<div class="form-inline">
<select id="sortEntriesBy" class="custom-select sortfilter-control"
data-ng-change="$ctrl.filterAndSortEntries()" data-ng-model="$ctrl.entryListModifiers.sortBy"
data-ng-options="item as $ctrl.entryListModifiers.sortOptionLabel(item.label) for item in $ctrl.entryListModifiers.sortOptions track by item.value">
</select>
</div>
</div>
<div class="form-group sortfilter-form">
<label class="font-weight-bold" for="sortEntriesBy">Advanced</label>

<section class="d-flex justify-content-around">
<label>
<input type="checkbox" data-ng-change="$ctrl.filterAndSortEntries()" data-ng-model="$ctrl.entryListModifiers.sortReverse" class="align-middle">
<span class="align-middle pl-1">Reverse</span>
</label>

<label>
<input type="checkbox" data-ng-change="$ctrl.filterAndSortEntries()" data-ng-model="$ctrl.entryListModifiers.wholeWord" class="align-middle">
<span class="align-middle pl-1">Whole word</span>
</label>
</section>
</div>
</div>
</div>
</div>
Expand Down
71 changes: 43 additions & 28 deletions src/angular-app/languageforge/lexicon/editor/editor-list.view.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,42 +31,57 @@
<span class="fa fa-times clear-search-button" data-ng-show="$ctrl.entryListModifiers.filterBy.text" data-ng-click="$ctrl.clearSearchText()"></span>
</div>
<button class="btn btn-sm" type="button" data-ng-click="$ctrl.toggleFilterOptions()">
<span class="options-active-icon fa fa-circle" data-ng-class="{'icon-active': $ctrl.filterSortOptionsActive()}"></span> Options
<span class="options-active-icon fa fa-circle" data-ng-class="{'icon-active': $ctrl.filterOptionsActive()}"></span> Options
<i class="fa" data-ng-class="$ctrl.show.entryListModifiers ? 'fa-angle-up': 'fa-angle-down'"></i>
</button>
</div>
</div>
</div>
<div class="row" data-ng-show="$ctrl.show.entryListModifiers">
<div class="col">
<div class="word-form-filters">
<div class="form-group sortfilter-form">
<div class="row no-gutters" data-ng-show="$ctrl.show.entryListModifiers">
<div class="col-12 col-lg-5">
<div class="word-form-filters h-100">
<div class="sortfilter-form">
<label class="font-weight-bold" for="filterEntriesFor">Filter Entries By</label>
<div class="form-inline">
<!--suppress HtmlFormInputWithoutLabel -->
<select class="custom-select sortfilter-control" data-ng-show="$ctrl.entryListModifiers.filterBy.option"
data-ng-change="$ctrl.filterAndSortEntries()" data-ng-model="$ctrl.entryListModifiers.filterType">
<option value="isEmpty">Doesn't have</option>
<option value="isNotEmpty">Has</option>
</select>
<select class="custom-select sortfilter-control" id="filterEntriesFor"
data-ng-change="$ctrl.filterAndSortEntries()" data-ng-model="$ctrl.entryListModifiers.filterBy.option"
data-ng-options="item as item.label for item in $ctrl.entryListModifiers.filterOptions track by item.key">
<option value="">Show All</option>
</select>
</div>
<select class="custom-select sortfilter-control ml-1" data-ng-show="$ctrl.entryListModifiers.filterBy.option"
data-ng-change="$ctrl.filterAndSortEntries()" data-ng-model="$ctrl.entryListModifiers.filterType">
<option value="isEmpty">Doesn't have</option>
<option value="isNotEmpty">Has</option>
</select>
<select class="custom-select sortfilter-control ml-1" id="filterEntriesFor"
data-ng-change="$ctrl.filterAndSortEntries()" data-ng-model="$ctrl.entryListModifiers.filterBy.option"
data-ng-options="item as item.label for item in $ctrl.entryListModifiers.filterOptions track by item.key">
<option value="">Show All</option>
</select>
</div>
<div class="form-group sortfilter-form">
</div>
</div>
<div class="col-12 col-md-8 col-lg-4">
<div class="word-form-filters h-100">
<div class="sortfilter-form">
<label class="font-weight-bold" for="sortEntriesBy">Sort Entries By</label>
<label class="reverse-label">
<input type="checkbox" data-ng-change="$ctrl.filterAndSortEntries()" data-ng-model="$ctrl.entryListModifiers.sortReverse"> Reverse
</label>
<div class="form-inline">
<select id="sortEntriesBy" class="custom-select sortfilter-control"
data-ng-change="$ctrl.filterAndSortEntries()" data-ng-model="$ctrl.entryListModifiers.sortBy"
data-ng-options="item as $ctrl.entryListModifiers.sortOptionLabel(item.label) for item in $ctrl.entryListModifiers.sortOptions track by item.value">
</select>
</div>
<select id="sortEntriesBy" class="custom-select sortfilter-control ml-1"
data-ng-change="$ctrl.filterAndSortEntries()" data-ng-model="$ctrl.entryListModifiers.sortBy"
data-ng-options="item as $ctrl.entryListModifiers.sortOptionLabel(item.label) for item in $ctrl.entryListModifiers.sortOptions track by item.value">
</select>
</div>
</div>
</div>
<div class="col-12 col-md-4 col-lg-3">
<div class="word-form-filters h-100">
<div class="sortfilter-form h-100">
<label class="font-weight-bold">Advanced</label>

<section class="d-flex justify-content-around">
<label>
<input type="checkbox" data-ng-change="$ctrl.filterAndSortEntries()" data-ng-model="$ctrl.entryListModifiers.sortReverse" class="align-middle">
<span class="align-middle pl-1">Reverse</span>
</label>

<label>
<input type="checkbox" data-ng-change="$ctrl.filterAndSortEntries()" data-ng-model="$ctrl.entryListModifiers.wholeWord" class="align-middle">
<span class="align-middle pl-1">Whole word</span>
</label>
</section>
</div>
</div>
</div>
Expand Down
68 changes: 20 additions & 48 deletions src/angular-app/languageforge/lexicon/editor/editor.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ export class LexiconEditorController implements angular.IController {
sortBy: this.$state.params.sortBy,
filterText: this.$state.params.filterText,
sortReverse: this.$state.params.sortReverse,
wholeWord: this.$state.params.wholeWord,
filterType: this.$state.params.filterType,
filterBy: this.$state.params.filterBy
}, { notify: true });
Expand Down Expand Up @@ -268,6 +269,7 @@ export class LexiconEditorController implements angular.IController {
}
}
this.entryListModifiers.sortReverse = this.$state.params.sortReverse === 'true';
this.entryListModifiers.wholeWord = this.$state.params.wholeWord === 'true';

if (this.$state.params.filterType) {
this.entryListModifiers.filterType = this.$state.params.filterType;
Expand Down Expand Up @@ -296,24 +298,27 @@ export class LexiconEditorController implements angular.IController {
sortBy: this.entryListModifiers.sortBy.label,
filterText: this.entryListModifiers.filterText(),
sortReverse: this.entryListModifiers.sortReverse,
wholeWord: this.entryListModifiers.wholeWord,
filterType: this.entryListModifiers.filterType,
filterBy: this.entryListModifiers.filterByLabel()
}, { notify: false });
this.editorService.filterAndSortEntries.apply(this, arguments);
}

filterSortOptionsActive() {
filterOptionsActive() {
const mod = this.entryListModifiers;
return mod.filterBy && mod.filterBy.option || mod.sortBy.value !== 'default' || mod.sortReverse;
return (mod.filterBy && mod.filterBy.option) || mod.sortBy.value !== 'default' || mod.sortReverse || mod.wholeWord
}

shouldShowFilterReset() {
const modifiers = this.entryListModifiers;
return modifiers.filterActive() || modifiers.sortBy.value !== 'default' || modifiers.sortReverse;
return modifiers.filterActive() || modifiers.sortBy.value !== 'default' || modifiers.sortReverse
}

resetEntryListFilter(): void {
this.entryListModifiers.filterBy = null;
this.entryListModifiers.wholeWord = false;

this.filterAndSortEntries();
}

Expand Down Expand Up @@ -413,15 +418,12 @@ export class LexiconEditorController implements angular.IController {
if (entry && isNewEntry) {
this.setCurrentEntry(this.entries[this.editorService.getIndexInList(entry.id, this.entries)]);
this.editorService.removeEntryFromLists(newEntryTempId);

if (doSetEntry) {
this.$state.go('.', {
entryId: entry.id,
sortBy: this.entryListModifiers.sortBy.label,
filterText: this.entryListModifiers.filterText(),
sortReverse: this.entryListModifiers.sortReverse,
filterType: this.entryListModifiers.filterType,
filterBy: this.entryListModifiers.filterByLabel()
}, { notify: false });

this.scrollListToEntry(entry.id, 'top');
}
}
Expand Down Expand Up @@ -475,7 +477,6 @@ export class LexiconEditorController implements angular.IController {
const newEntry = new LexEntry();
newEntry.id = uniqueId;
this.setCurrentEntry(newEntry);
// noinspection JSIgnoredPromiseFromCall - comments will load in the background
this.commentService.loadEntryComments(newEntry.id);
this.editorService.addEntryToEntryList(newEntry);
this.editorService.showInitialEntries().then(() => {
Expand All @@ -498,14 +499,6 @@ export class LexiconEditorController implements angular.IController {
iShowList--;
}
this.setCurrentEntry(this.visibleEntries[iShowList]);
this.$state.go('.', {
entryId: this.visibleEntries[iShowList].id,
sortBy: this.entryListModifiers.sortBy.label,
filterText: this.entryListModifiers.filterText(),
sortReverse: this.entryListModifiers.sortReverse,
filterType: this.entryListModifiers.filterType,
filterBy: this.entryListModifiers.filterByLabel()
}, { notify: false });
} else {
this.returnToList();
}
Expand Down Expand Up @@ -927,6 +920,7 @@ export class LexiconEditorController implements angular.IController {
sortBy: this.entryListModifiers.sortBy.label,
filterText: this.entryListModifiers.filterText(),
sortReverse: this.entryListModifiers.sortReverse,
wholeWord: this.entryListModifiers.wholeWord,
filterType: this.entryListModifiers.filterType,
filterBy: this.entryListModifiers.filterByLabel()
}, { notify: false });
Expand All @@ -936,6 +930,7 @@ export class LexiconEditorController implements angular.IController {
sortBy: this.$state.params.sortBy,
filterText: this.$state.params.filterText,
sortReverse: this.$state.params.sortReverse,
wholeWord: this.$state.params.wholeWord,
filterType: this.$state.params.filterType,
filterBy: this.$state.params.filterBy
});
Expand Down Expand Up @@ -1215,14 +1210,14 @@ export class LexiconEditorController implements angular.IController {
const posOffset = (position === 'top') ? 274 : 487;
const entryDivId = '#entryId_' + id;
const listDivId = '#compactEntryListContainer';
let index;
let index = this.editorService.getIndexInList(id, this.filteredEntries);

// make sure the item is visible in the list
// todo implement lazy "up" scrolling to make this more efficient

// only expand the "show window" if we know that the entry is actually in
// the entry list - a safe guard
if (this.editorService.getIndexInList(id, this.filteredEntries) != null) {
if (index != null) {
while (this.visibleEntries.length < this.filteredEntries.length) {
index = this.editorService.getIndexInList(id, this.visibleEntries);
if (index != null) {
Expand All @@ -1231,8 +1226,6 @@ export class LexiconEditorController implements angular.IController {

this.editorService.showMoreEntries();
}
} else {
console.warn('Error: tried to scroll to an entry that is not in the entry list!');
}

// note: ':visible' is a JQuery invention that means 'it takes up space on
Expand All @@ -1250,34 +1243,13 @@ export class LexiconEditorController implements angular.IController {
}

private static scrollDivToId(containerId: string, divId: string, posOffset: number = 0): void {
const $containerDiv: any = $(containerId);
let $div: any = $(divId);
let foundDiv: boolean = false;
let offsetTop: number = 0;

// todo: refactor this spaghetti logic
if ($div && $containerDiv) {
if ($div.offsetTop == null) {
if ($div[0] != null) {
$div = $div[0];
foundDiv = true;
} else {
console.log('Error: unable to scroll to div with div id ' + divId);
}
}

if (foundDiv) {
if ($div.offsetTop == null) {
offsetTop = $div.offset().top - posOffset;
} else {
offsetTop = $div.offsetTop - posOffset;
}
const $containerDiv: any = $(containerId)
const $div: any = $(divId)[0];
longrunningprocess marked this conversation as resolved.
Show resolved Hide resolved

if ($div && $containerDiv.scrollTop) {
let offsetTop: number = $div.offsetTop - posOffset;

if (offsetTop < 0) {
offsetTop = 0;
}
$containerDiv.scrollTop(offsetTop);
}
$containerDiv.scrollTop(offsetTop > -1 ? offsetTop : 0);
}
}

Expand Down
Loading