Skip to content

Commit

Permalink
Merge branch 'feature/LF3135/isListHtml' into 'master'
Browse files Browse the repository at this point in the history
Feature/lf3135/is list html

See merge request lfor/autocomplete-lhc!172
  • Loading branch information
jcy1225 committed Nov 6, 2024
2 parents 3fef830 + 971cd1b commit dd7b725
Show file tree
Hide file tree
Showing 14 changed files with 333 additions and 117 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
This log documents significant changes for each release. This project follows
[Semantic Versioning](http://semver.org/).

## [19.3.0] - 2024-10-10
### Added
- new Prefetch option: isListHTML. Defaults to false. When set to true, display
the list as HTML.

## [19.2.4] - 2024-04-17
### Added
- Announce that the search is in progress if it takes more than 1.5 seconds.
Expand Down
2 changes: 1 addition & 1 deletion bower.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "autocomplete-lhc",
"version": "19.2.4",
"version": "19.3.0",
"main": [
"source/auto_completion.css",
"source/polyfill.js",
Expand Down
206 changes: 97 additions & 109 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "autocomplete-lhc",
"version": "19.2.4",
"version": "19.3.0",
"description": "",
"main": "source/index.js",
"config": {
Expand Down
63 changes: 58 additions & 5 deletions source/autoCompPrefetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@
*/
rawList_: null,

/**
* The raw list stripped of HTML tags. Used only when isListHTML is true.
*/
rawListWithoutTags_: null,

/**
* An array of the codes for the items in the list.
*/
Expand Down Expand Up @@ -89,6 +94,11 @@
*/
autoFill_: true,

/**
* If true, the list is displayed as HTML.
*/
isListHTML_: false,

/**
* The constructor. (See Prototype's Class.create method.)
*
Expand Down Expand Up @@ -140,6 +150,12 @@
* <li>formattedListItems - an optional HTML formatted list of descriptions.
* When provided, the descriptions will be appended to corresponding items
* for display. Filtering does not cover content in this formatted list.</li>
* <li>isListHTML - Defaults to false. When set to true, display the list
* as HTML. This should only be used when you know the list content can be
* safely displayed as HTML. There should not be "<" or ">" in the text
* content as filtering might not work as expected. With multi-select, make
* sure the list values, which are texts with HTML tags removed, are unique
* in the list.</li>
* </ul>
*/
initialize: function(id, listItems, options) {
Expand All @@ -160,6 +176,9 @@
var autoFill = options['autoFill'];
if (autoFill !== undefined)
this.autoFill_ = autoFill;
var isListHTML = options['isListHTML'];
if (isListHTML !== undefined)
this.isListHTML_ = isListHTML;

// Call the base class' initialize method. We do this via the "apply"
// function, which lets us specify the "this" object plus an array of
Expand Down Expand Up @@ -292,6 +311,26 @@
},


/**
* Checks whether an HTML string has equal number of '<' and'>', so the end of the string
* is not inside an HTML tag.
* @param value an HTML string.
* @returns true if there are an equal number of '<' and '>', false otherwise.
*/
isHtmlTagsClosed_: function(value) {
const openTagCount = (value.match(/</g) || []).length;
const closeTagCount = (value.match(/>/g) || []).length;
if (openTagCount === closeTagCount) {
return true;
} else if (openTagCount - closeTagCount === 1) {
return false;
} else {
console.error("The numbers of opening and closing tags might not be right: " + value);
return false;
}
},


/**
* Generates the list of items that match the user's input. This was
* copied from the Scriptaculous controls.js Autocompleter.Local.prototype
Expand Down Expand Up @@ -331,9 +370,13 @@
var headerCount = 0;
var headingsShown = 0;
var skippedSelected = 0; // items already selected that are left out of the list
var escapeHTML = Def.Autocompleter.Base.escapeAttribute;
var isListHTML = instance.options.isListHTML === true;
var escapeHTML = isListHTML ? x => x : Def.Autocompleter.Base.escapeAttribute;

if (instance.options.ignoreCase)
entry = entry.toLowerCase();
if (isListHTML)
entry = Def.Autocompleter.Base.escapeAttribute(entry);
var formattedListItems = instance.options.formattedListItems;
for (var i=0, max=instance.rawList_.length; i<max; ++i) {
var tmp = instance.indexToHeadingLevel_[i];
Expand Down Expand Up @@ -402,8 +445,10 @@
}
else { // foundPos > 0
// See if the match is at a word boundary
if (instance.options.fullSearch ||
/(.\b|_)./.test(elemComp.substr(foundPos-1,2))) {
if ((instance.options.fullSearch ||
/(.\b|_)./.test(elemComp.substr(foundPos-1,2))) &&
// See if the match is inside an HTML tag, when isListHTML is true
(!isListHTML || instance.isHtmlTagsClosed_(elemComp.substr(0, foundPos)))) {
++totalCount;
foundMatch = true;
if (totalCount <= maxReturn) {
Expand All @@ -424,7 +469,7 @@

var alreadySelected = false;
if (instance.multiSelect_) {
alreadySelected = instance.isSelected(rawItemText)
alreadySelected = instance.isSelected(instance.isListHTML_ ? instance.rawListWithoutTags_[i] : rawItemText);
if (alreadySelected)
++skippedSelected;
}
Expand Down Expand Up @@ -555,8 +600,16 @@
this.listIsOriginal_ = false;
var numItems = listItems.length;
this.rawList_ = new Array(numItems);
if (this.isListHTML_) {
this.rawListWithoutTags_ = new Array(numItems);
}
for (var r=0, max=listItems.length; r<max; ++r) {
this.rawList_[r] = listItems[r].trim();
if (this.isListHTML_) {
// If list is displayed as HTML, remove tags and just keep text for
// list value to display in input.
this.rawListWithoutTags_[r] = listItems[r].replace(/(<([^>]+)>)/gi, "").trim();
}
}

var displayList = new Array(numItems);
Expand Down Expand Up @@ -973,7 +1026,7 @@
// looked up from the raw list, according to the autocompRawListIndex
// attribute assigned earlier.
const autocompleteIndex = li.getAttribute('autocompRawListIndex');
const value = this.rawList_[autocompleteIndex];
const value = this.isListHTML_ ? this.rawListWithoutTags_[autocompleteIndex] : this.rawList_[autocompleteIndex];
return value;
},

Expand Down
3 changes: 3 additions & 0 deletions test/cypress/integration/autocompleters.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import {createInputElement, createListWithHeadings, extractListVals} from '../su

describe('autocompleters', function () {
var listSelectionItemData_ = {};
Cypress.on('uncaught:exception', (err, runnable) => {
return false;
});

beforeEach(function () {
cy.visit(TestPages.autocomp_test);
Expand Down
18 changes: 18 additions & 0 deletions test/cypress/integration/multiSelect.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,5 +257,23 @@ describe('multi-select lists', function() {
cy.get(po.multiPrefetchCWE).should('not.have.class', 'ansList');
cy.get(po.multiPrefetchCWE).should('have.class', 'test-class');
});

it('should remove already selected items from HTML lists', function () {
po.openTestPage();
// Add a list value
cy.get(po.multiPrefetchHtml).click();
cy.get(po.allSearchRes).should('have.length', 3);
po.searchResult(1).click();
cy.get(po.multiPrefetchHtmlSelected).should('have.length', 1);
// Should see the selected item removed from list when focus comes back to the field.
cy.get(po.nonField).click(); // shift focus from field
cy.get(po.multiPrefetchHtml).click();
cy.get(po.allSearchRes).should('have.length', 2);
// Remove a list value
cy.get(po.multiPrefetchHtmlFirstSelected).click();
cy.get(po.multiPrefetchHtmlSelected).should('have.length', 0);
cy.get(po.multiPrefetchHtml).click();
cy.get(po.allSearchRes).should('have.length', 3);
});
});

82 changes: 82 additions & 0 deletions test/cypress/integration/prefetchLists.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,5 +172,87 @@ describe('Prefetch lists', function() {
cy.get(po.prefetchCWE).click();
cy.get(po.searchResSel).should('be.visible');
});

it('should display HTML when isListHTML is true', function() {
po.openTestPage();
cy.get('#prefetch_html')
.focus();
cy.get('#completionOptions li')
.as('listOptions');
cy.get('@listOptions')
.should('be.visible')
.should('have.length', 3);
cy.get('@listOptions').eq(0).should('have.html', '<strong>bla</strong>');
cy.get('@listOptions').eq(1).should('have.html', '<span title="I am strong.">bla</span>');
cy.get('@listOptions').eq(2).should('have.html', '<i title="I am strong.">I am strong</i>');
// Check the value in the field after the user selects something.
cy.get('@listOptions').eq(2).click();
cy.get('#prefetch_html').should('have.value', 'I am strong');
});

it('should filter and highlight correctly when isListHTML is true', function() {
po.openTestPage();
cy.get('#prefetch_html')
.focus();
cy.get('#completionOptions li')
.should('have.length', 3);
// Should only match and highlight the text outside an HTML tag.
cy.get('#prefetch_html')
.type('strong');
cy.get('#completionOptions li')
.should('have.length', 1);
cy.get('#completionOptions li')
.eq(0)
.should('have.html', '<i title="I am strong.">I am <strong>strong</strong></i>');
// The value in the field should be the original HTML option after the user selects the highlighted option.
cy.get('#completionOptions li')
.eq(0)
.click();
cy.get('#prefetch_html').should('have.value', 'I am strong');
});

it('should not return a match for "bla<" when isListHTML is true', function() {
po.openTestPage();
cy.get('#prefetch_html')
.focus();
cy.get('#completionOptions li')
.should('have.length', 3);
cy.get('#prefetch_html')
.type('bla');
cy.get('#completionOptions li')
.should('have.length', 2);
cy.get('#prefetch_html')
.type('<');
cy.get('#completionOptions li')
.should('have.length', 0);
});

it('should display images in drop-down when isListHTML is true', function() {
po.openTestPage();
cy.get('#prefetch_html_image')
.focus();
cy.get('#completionOptions img')
.should('have.length', 3);
// Check the value in the field after the user selects something.
cy.get('#completionOptions li').eq(2).click();
cy.get('#prefetch_html_image').should('have.value', 'Sad');
});

it('should display text in drop-down when isListHTML is false', function() {
po.openTestPage();
cy.get('#prefetch_non_html_image')
.focus();
// Autocomplete should display the options as plain text, no <img> tags.
cy.get('#completionOptions img')
.should('have.length', 0);
cy.get('#completionOptions li')
.should('have.length', 4);
cy.get('#completionOptions li')
.eq(0)
.should('have.text', 'Happy <img src="happy-face.png">');
// Check the value in the field after the user selects something.
cy.get('#completionOptions li').eq(0).click();
cy.get('#prefetch_non_html_image').should('have.value', 'Happy <img src="happy-face.png">');
});
});

9 changes: 9 additions & 0 deletions test/cypress/support/autocompPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ var AutocompPage = function() {
this.multiPrefetchCWESelected =
multiPrefetchCWESectionCSS + ' button';

// Multi-select HTML prefetch list
var multiPrefetchHtmlSectionCSS = '#multiPrefetchHtmlSection';
this.multiPrefetchHtmlID = 'prefetch_html_multi';
this.multiPrefetchHtml = '#' + this.multiPrefetchHtmlID;
this.multiPrefetchHtmlFirstSelected =
multiPrefetchHtmlSectionCSS + ' li:first-child button';
this.multiPrefetchHtmlSelected =
multiPrefetchHtmlSectionCSS + ' button';

// Multi-select CWE search list
var multiSearchCWESectionCSS = '#multiSearchCWESection';
this.multiSearchCWEID = 'multi_sel_search_cwe';
Expand Down
20 changes: 20 additions & 0 deletions test/pages/autocomp_atr.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
input[type="text"] { width: 20em }
body {margin: 0; font-family: Verdana, Geneva, sans-serif}
#content {margin: 8px}
#searchResults img {vertical-align: text-bottom;}
</style>
</head>

Expand Down Expand Up @@ -45,6 +46,19 @@
A Prefetch CNE autocompleter with a default:&nbsp;&nbsp;</label><input autocomplete="off" id="prefetch_default_cne" name="prefetch_default_cne" type="text" />
</div>

<div class="field">
<label for="prefetch_html" id="prefetch_html_lbl">
A Prefetch autocompleter showing HTML items:&nbsp;&nbsp;</label><input autocomplete="off" id="prefetch_html" type="text" />
</div>
<div class="field">
<label for="prefetch_html_image" id="prefetch_html_image_lbl">
A Prefetch autocompleter showing HTML items with images:&nbsp;&nbsp;</label><input autocomplete="off" id="prefetch_html_image" type="text" />
</div>
<div class="field">
<label for="prefetch_non_html_image" id="prefetch_non_html_image_lbl">
A Prefetch autocompleter showing HTML image tags with HTML disabled:&nbsp;&nbsp;</label><input autocomplete="off" id="prefetch_non_html_image" type="text" />
</div>

<div class="field">
<label for="fe_search_cne" id="fe_search_cne_lbl">
A search CNE autocompleter:&nbsp;&nbsp;</label>
Expand Down Expand Up @@ -176,6 +190,12 @@ <h2>Multi-select lists</h2>
</div>
<button id="dest_multi_sel_cwe" type="button">Destroy autocompleter</button>

<div class="field" id="multiPrefetchHtmlSection">
<label for="prefetch_html_multi" id="prefetch_html_multi_lbl">
A Prefetch multi-select autocompleter showing HTML items:&nbsp;&nbsp;</label>
<input autocomplete="off" id="prefetch_html_multi" class="test-class" type="text" />
</div>

<div class="field" id="multiSearchCWESection">
<label for="multi_sel_search_cwe" id="multi_sel_search_cwe_lbl">
A Search CWE multi-select autocompleter:&nbsp;&nbsp;</label>
Expand Down
40 changes: 39 additions & 1 deletion test/pages/autocompleter_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,34 @@ opts['autoFill']=true
var fe_prefetch_cwe_autoComp = new Def.Autocompleter.Prefetch('prefetch_cwe',
["Spanish","French","Other", "escape<test>&"], opts);
var opts = {};
opts['addSeqNum'] = false;
opts['isListHTML'] = true;
var fe_prefetch_html_autoComp = new Def.Autocompleter.Prefetch('prefetch_html',
[
'<strong>bla</strong>',
'<span title="I am strong.">bla</span>',
'<i title="I am strong.">I am strong</i>'
], opts);
var opts = {};
opts['isListHTML'] = true;
var fe_prefetch_html_image_autoComp = new Def.Autocompleter.Prefetch('prefetch_html_image',
[
'Happy <img src="happy-face.png">',
'Neutral <img src="neutral-face.png">',
'Sad <img src="sad-face.png">',
'one < two'
], opts);
var opts = {};
opts['isListHTML'] = false;
opts['addSeqNum'] = false;
var fe_prefetch_non_html_image_autoComp = new Def.Autocompleter.Prefetch('prefetch_non_html_image',
[
'Happy <img src="happy-face.png">',
'Neutral <img src="neutral-face.png">',
'Sad <img src="sad-face.png">',
'one < two'
], opts);
var opts = {};
opts['matchListValue']=true
opts['autocomp']=true
opts['showLoadingIndicator']=false
Expand Down Expand Up @@ -201,7 +229,17 @@ var fe_multi_sel_cne_autoComp =
new Def.Autocompleter.Prefetch('multi_sel_cwe', ["Spanish","French","Other"], opts);
document.querySelector('#dest_multi_sel_cwe').addEventListener('click', (event)=>{
fe_multi_sel_cne_autoComp.destroy()});

// multi-select prefetch list with HTML
var opts = {};
opts['addSeqNum'] = false;
opts['isListHTML'] = true;
opts['maxSelect'] = '*';
var fe_prefetch_html_multi_autoComp = new Def.Autocompleter.Prefetch('prefetch_html_multi',
[
'<strong>foo</strong>',
'<span title="I am strong.">bar</span>',
'Happy <img src="happy-face.png">'
], opts);
// multi-select search list without match required
opts = {
'matchListValue': false,
Expand Down
Binary file added test/pages/happy-face.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/pages/neutral-face.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/pages/sad-face.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit dd7b725

Please sign in to comment.