Skip to content

Commit

Permalink
deal better with same values
Browse files Browse the repository at this point in the history
  • Loading branch information
lekoala committed Jun 15, 2023
1 parent d762f1c commit 38549b3
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 66 deletions.
5 changes: 3 additions & 2 deletions demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ <h1>Demo</h1>
<label for="validationTags" class="form-label">Tags (check console to see selected values). It's sortable thanks to SortableJS (in
demo, it breaks text selection) !</label>
<div class="position-relative has-validation">
<select class="form-select" id="validationTags" name="tags[]" multiple data-allow-clear="dsfds" data-allow-same="1" required>
<select class="form-select" id="validationTags" name="tags[]" multiple data-allow-clear="dsfds" required>
<option selected="selected" disabled hidden value="">Choose a tag...</option>
<option value="1" selected="selected">Apple</option>
<option value="2" selected="selected">Banana</option>
Expand Down Expand Up @@ -444,6 +444,7 @@ <h1>Demo</h1>
<optgroup label="fruits">
<option value="1" selected="selected" title="my tooltip">Apple</option>
<option value="2" title="my tooltip for banana">Banana</option>
<option value="2" title="my tooltip for banana">Banana</option>
<option value="2" title="my tooltip for banana">Banana 2 same value</option>
<option value="3" title="my tooltip">Orange</option>
</optgroup>
Expand Down Expand Up @@ -575,7 +576,7 @@ <h1>Demo</h1>
<option value="[email protected]" selected="selected">[email protected]</option>
<option value="[email protected]" data-name="Mr. X">[email protected]</option>
<option value="[email protected]" data-name="Ms. X">[email protected]</option>
<option value="some_value">Some invalid yet callable input</option>
<option value="some_value" data-name="no name">Some invalid yet callable input</option>
</select>
<div class="invalid-feedback">Please select only @mycompany.com addresses.</div>
</div>
Expand Down
67 changes: 35 additions & 32 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
]
},
"devDependencies": {
"@happy-dom/global-registrator": "^9.10.2",
"@happy-dom/global-registrator": "^9.20",
"ava": "^5.2.0",
"bootstrap": "^5.3.0",
"esbuild": "^0.17.18"
Expand Down
97 changes: 67 additions & 30 deletions tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -1300,7 +1300,7 @@ class Tags {

/**
* @param {Suggestion} suggestion
* @param {Number} i
* @param {Number} i The global counter
*/
_buildSuggestionsItem(suggestion, i) {
if (!suggestion[this._config.valueField]) {
Expand Down Expand Up @@ -1361,9 +1361,13 @@ class Tags {
return this._selectElement.querySelectorAll("option[data-init]");
}

/**
* Call this before looping in a list that calls addItem
* This will make sure addItem will not add incorrectly options to the select
*/
_removeSelectedAttrs() {
this._selectElement.querySelectorAll("option").forEach((opt) => {
opt.removeAttribute("selected");
rmAttr(opt, "selected");
});
}

Expand Down Expand Up @@ -1507,8 +1511,7 @@ class Tags {

const lookup = normalize(this._searchInput.value);

// Get current values
const values = this.getSelectedValues();
const valueCounter = {};

// Filter the list according to search string
const list = this._dropElement.querySelectorAll("li");
Expand Down Expand Up @@ -1541,9 +1544,15 @@ class Tags {
link.classList.remove(...this._activeClasses());

// Hide selected values
if (!this._config.allowSame && values.indexOf(link.getAttribute(VALUE_ATTRIBUTE)) != -1) {
hideItem(item);
continue;
if (!this._config.allowSame) {
const v = link.getAttribute(VALUE_ATTRIBUTE);
// Find if the matching option is already selected by index to deal with same values
valueCounter[v] = valueCounter[v] || 0;
const opt = this._findOption(link.getAttribute(VALUE_ATTRIBUTE), "[selected]", valueCounter[v]++);
if (opt) {
hideItem(item);
continue;
}
}

// Check search length since we can trigger dropdown with arrow
Expand Down Expand Up @@ -1746,11 +1755,22 @@ class Tags {
* @returns {Boolean}
*/
_isSelected(text) {
const opt = Array.from(this._selectElement.querySelectorAll("option")).find((el) => el.textContent == text);
if (opt && opt.getAttribute("selected")) {
return true;
}
return false;
const opt = Array.from(this._selectElement.querySelectorAll("option")).find(
(el) => el.textContent == text && el.getAttribute("selected")
);
return opt ? true : false;
}

/**
* Find if label is selectable (based on attribute)
* @param {string} text
* @returns {Boolean}
*/
_isSelectable(text) {
const opt = Array.from(this._selectElement.querySelectorAll("option")).find(
(el) => el.textContent == text && !el.getAttribute("selected")
);
return opt ? true : false;
}

/**
Expand Down Expand Up @@ -1877,13 +1897,24 @@ class Tags {
if (!text) {
return false;
}
// Doesn't allow new
if (data.new && !this._config.allowNew) {
return false;
}
// Check disabled
if (this.isDisabled()) {
return false;
}
// Check already selected input (single will replace, so never return false if selected)
if (!this.isSingle() && !this._config.allowSame && this._isSelected(text)) {
return false;
if (!this.isSingle() && !this._config.allowSame) {
// For new tags, check if selected
if (data.new && this._isSelected(text)) {
return false;
}
// For existing tags, check if selectable
if (!data.new && !this._isSelectable(text)) {
return false;
}
}
// Check for max
if (this.isMaxReached()) {
Expand Down Expand Up @@ -1939,6 +1970,22 @@ class Tags {
this._resetHtmlState();
}

/**
* Keep in mind that we can have the same value for multiple options
* @param {*} value
* @param {string} mode
* @param {number} counter
* @returns {HTMLOptionElement|null}
*/
_findOption(value, mode = "", counter = 0) {
// escape invalid characters for HTML attributes: \' " = < > ` &.'
const escapedValue = CSS.escape(value);
const sel = 'option[value="' + escapedValue + '"]' + mode;
const opts = this._selectElement.querySelectorAll(sel);
//@ts-ignore
return opts[counter] || null;
}

/**
* You might want to use canAdd before to ensure the item is valid
* @param {string} text
Expand All @@ -1956,14 +2003,7 @@ class Tags {
this.removeLastItem(true);
}

// Keep in mind that we can have the same value for multiple options
// escape invalid characters for HTML attributes: \' " = < > ` &.'
const escapedValue = CSS.escape(value);
const sel = 'option[value="' + escapedValue + '"]:not([selected])';
/**
* @type {HTMLOptionElement}
*/
let opt = this._selectElement.querySelector(sel);
let opt = this._findOption(value, ":not([selected])");

// we need to create a new option
if (!opt) {
Expand Down Expand Up @@ -2136,18 +2176,15 @@ class Tags {
// Remove badge if any
// escape invalid characters for HTML attributes: \' " = < > ` &.'
const escapedValue = CSS.escape(value);
let item = this._containerElement.querySelector("span[" + VALUE_ATTRIBUTE + '="' + escapedValue + '"]');
if (!item) {
let items = this._containerElement.querySelectorAll("span[" + VALUE_ATTRIBUTE + '="' + escapedValue + '"]');
if (!items.length) {
return;
}
item.remove();
const idx = items.length - 1;
items[idx].remove();

// update select
/**
* @type {HTMLOptionElement}
*/
let opt = this._selectElement.querySelector('option[value="' + escapedValue + '"][selected]');

let opt = this._findOption(value, "[selected]", idx);
if (opt) {
rmAttr(opt, "selected");
opt.selected = false;
Expand Down
11 changes: 10 additions & 1 deletion test/tags.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ let holder = document.createElement("div");
let el = document.createElement("select");
el.setAttribute("placeholder", "Test placeholder");
el.setAttribute("multiple", "multiple");
el.dataset.allowNew = "true";

// let opt = document.createElement("option");
// opt.value = "addfirst";
// opt.innerText = "add first";
// el.appendChild(opt);

holder.appendChild(el);
form.appendChild(holder);

Expand Down Expand Up @@ -74,9 +81,11 @@ test("it prevents adding if necessary", (t) => {
let maxTags = Tags.getInstance(maxEl);
let regularTags = Tags.getInstance(el);

t.truthy(regularTags.canAdd("addfirst"));
t.truthy(regularTags.canAdd("addfirst", { new: 1 }));
t.falsy(regularTags.canAdd("addfirst"));
regularTags.addItem("addfirst");
t.falsy(regularTags.canAdd("addfirst"));
t.falsy(regularTags.canAdd("addfirst", { new: 1 }));
t.falsy(regularTags.canAdd(""));

t.falsy(disabledTags.canAdd("test"));
Expand Down

0 comments on commit 38549b3

Please sign in to comment.