Last active
February 8, 2023 08:34
-
-
Save psaliente/e0913668ee171966712516504315b951 to your computer and use it in GitHub Desktop.
custom knockout js component inspired by jbt-tagbox (https://github.com/jbt/tagbox)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function ($) { | |
$.fn.hasScrollBar = function () { | |
return this.get(0) ? this.get(0).scrollHeight > this.innerHeight() : false; | |
}; | |
} (jQuery)); | |
function KOTagComponent(params) { | |
var self = this; | |
self.version = "1.1.3"; | |
self.fuzzyMatch = function (item, term, relevances) { | |
function escapeRegex(term) { | |
return term.replace(/[\[\]\{\}\(\)\^\$\.\*\+\|]/g, function (a) { | |
return '\\' + a; | |
}); | |
} | |
var UPPER = 0, LOWER = 1, NUMBER = 2, COMMON_DELIMS = 3, OTHER = 4, | |
relevanceMatrix = [ | |
[0, 240, 120, 240, 220], | |
[20, 0, 20, 120, 120], | |
[140, 140, 0, 140, 140], | |
[120, 120, 120, 0, 120], | |
[120, 120, 120, 160, 0] | |
], | |
stripAccents; | |
function charType(c) { | |
if (/[a-z]/.test(c)) { return LOWER; } | |
if (/[A-Z]/.test(c)) { return UPPER; } | |
if (/[0-9]/.test(c)) { return NUMBER; } | |
if (/[\/\-_\.]/.test(c)) { return COMMON_DELIMS; } | |
return OTHER; | |
} | |
function compareCharacters(theChar, before, after) { | |
var theType = charType(theChar), | |
beforeType = charType(before), | |
afterType = charType(after); | |
return relevanceMatrix[theType][beforeType] + | |
0.4 * relevanceMatrix[theType][afterType]; | |
} | |
stripAccents = (function (accented, unaccented) { | |
var matchRegex = new RegExp('[' + accented + ']', 'g'), translationTable = {}, i, | |
lookup = function (chr) { | |
return translationTable[chr] || chr; | |
}; | |
for (i = 0; i < accented.length; i += 1) { | |
translationTable[accented.charAt(i)] = unaccented.charAt(i); | |
} | |
return function (str) { | |
return str.replace(matchRegex, lookup); | |
}; | |
} ('àáâãäçèéêëìíîïñòóôõöùúûüýÿÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝ', | |
'aaaaaceeeeiiiinooooouuuuyyAAAAACEEEEIIIINOOOOOUUUUY')); | |
function bestRank(item, term, startingFrom) { | |
if (term.length === 0) { return startingFrom * 100 / item.length; } | |
if (item.length === 0) { return -1; } | |
if (!item.slice(startingFrom).match(new RegExp(('^.*' + escapeRegex(term.split('').join('~~K~~')) + '.*$').split('~~K~~').join('.*'), 'i'))) { | |
return -1; | |
} | |
var firstSearchChar = term.charAt(0), bestRankSoFar = -1, highlights, i, j, subsequentRank, characterScore, subsequentHighlights; | |
for (i = startingFrom; i < item.length; i += 1) { | |
if (item.charAt(i).toLowerCase() !== firstSearchChar.toLowerCase()) { continue; } | |
subsequentRank = bestRank(item.substr(i), term.slice(1), 1); | |
if (subsequentRank === -1) { continue; } | |
characterScore = 400 / Math.max(1, i); | |
if (item.substr(i).toLowerCase().indexOf(term.toLowerCase()) === 0) { characterScore += 3 * term.length * term.length; } | |
characterScore += compareCharacters( | |
item.charAt(i), | |
i === 0 ? '/' : item.charAt(i - 1), | |
i === item.length - 1 ? '/' : item.charAt(i + 1) | |
); | |
characterScore += subsequentRank; | |
if (characterScore > bestRankSoFar) { | |
bestRankSoFar = characterScore; | |
highlights = [i]; | |
subsequentHighlights = subsequentRank.highlights || []; | |
for (j = 0; j < subsequentHighlights.length; j += 1) { | |
highlights.push(subsequentHighlights[j] + i); | |
} | |
} | |
} | |
return { | |
__score: bestRankSoFar, | |
valueOf: function () { return this.__score; }, | |
highlights: highlights | |
}; | |
} | |
function fuzzyScoreStr(item, term) { | |
return bestRank(stripAccents(item), stripAccents(term), 0); | |
} | |
function fuzzyScore(item, term, relevances) { | |
if (typeof item == 'string') { return fuzzyScoreStr(item, term); } | |
var result = { | |
__score: 0, | |
valueOf: function () { return this.__score; }, | |
highlights: {} | |
}, i, thatScore; | |
for (i in relevances) { | |
if (!relevances.hasOwnProperty(i) || !item.hasOwnProperty(i)) { continue; } | |
thatScore = fuzzyScoreStr(item[i], term); | |
result.__score += relevances[i] * thatScore; | |
result.highlights[i] = thatScore > 0 ? thatScore.highlights : []; | |
} | |
return result; | |
} | |
return fuzzyScore(item, term, relevances); | |
}; | |
self.searchKey = ko.observable(); | |
self.activeIndex = ko.observable(0); | |
self.propertyKey = params.propertyKey || ''; | |
self.propertyText = params.propertyText || ''; | |
self.inputHasFocus = ko.observable(false); | |
self.dropdownHasFocus = ko.observable(false); | |
self.items = params.items; | |
self.searchIn = ko.observableArray(params.searchIn || []); | |
self.selectedItems = params.value; | |
self.onSelect = params.onSelect; | |
function scoresObject() { | |
var si = self.searchIn(), scores = {}, i; | |
for (i = 0; i < si.length; i += 1) { | |
scores[si[i]] = 10; | |
} | |
return scores; | |
} | |
self.suggestions = ko.computed(function () { | |
var alreadyExist = function (val) { var len = self.selectedItems().length; while (len--) { if (self.selectedItems()[len][self.propertyKey] === val) return true; } }, i, tmpsuggestions = [], item, term, score; | |
term = self.searchKey() || ''; | |
if (self.items()) { | |
for (i = 0; i < self.items().length; i += 1) { | |
item = self.items()[i]; | |
score = self.fuzzyMatch(item, term, scoresObject()); | |
if (!alreadyExist(item[self.propertyKey]) && (!term || score > 0)) { | |
tmpsuggestions.push(item); | |
} | |
} | |
} | |
if (Lazy) { | |
tmpsuggestions = Lazy(tmpsuggestions).sortBy(function (item) { return item.Name; }).toArray(); | |
} | |
return tmpsuggestions; | |
}); | |
self.deleteItem = function (item) { | |
self.selectedItems.remove(item); | |
}; | |
self.selectItem = function (item, event) { | |
if (self.onSelect) { | |
self.onSelect(item, event); | |
} | |
self.selectedItems.push(item); | |
self.searchKey(""); | |
self.dropdownHasFocus(false); | |
}; | |
self.showdropdown = function (index) { self.dropdownHasFocus(true); self.activeIndex(index); }; | |
self.hidedropdown = function (data, event) { self.dropdownHasFocus(false); }; | |
self.handleKeyEvent = function (value, event) { | |
var position = self.activeIndex(), | |
$suggestions = $(event.target).parent().siblings('.kotag-suggestions'), | |
scrollheight = $suggestions.get(0).scrollHeight, | |
itemheight = parseInt(scrollheight / self.suggestions().length, 10); | |
if (event.keyCode == 13) { | |
self.selectItem(self.suggestions()[position]); | |
} else if (event.keyCode == 38) { | |
position--; | |
} else if (event.keyCode == 40) { | |
position++; | |
} | |
position = position < 0 ? 0 : (position > (self.suggestions().length - 1) ? (self.suggestions().length - 1) : position); | |
if ($suggestions.hasScrollBar()) { | |
$suggestions.scrollTop(position * itemheight); | |
} | |
self.activeIndex(position); | |
return true; | |
}; | |
} | |
ko.components.register('component-kotag', { | |
viewModel: KOTagComponent, | |
template: '<div class="component-kotag">\ | |
<div class="kotag-fake-input">\ | |
<div data-bind="foreach: $component.selectedItems">\ | |
<div class="kotag-token"><span data-bind="text: $data.Name"></span> <a href="#" data-bind="click: $component.deleteItem">×</a></div>\ | |
</div>\ | |
<input type="text" data-bind="textInput: $component.searchKey, event: { keydown: $component.handleKeyEvent }, hasFocus: $component.inputHasFocus" class="kotag-input"/>\ | |
</div>\ | |
<div data-bind="visible: $component.searchKey() || $component.inputHasFocus() || $component.dropdownHasFocus(), event: { mouseleave: $component.hidedropdown }" class="kotag-suggestions">\ | |
<!-- ko foreach: $component.suggestions -->\ | |
<div class="kotag-suggestion" data-bind="click: $component.selectItem, css: { \'kotag-suggestion-active\': $component.activeIndex() === $index() }, event: { mouseover: $component.showdropdown.bind($data, $index()) }">\ | |
<span data-bind="text: $data[$component.propertyText]"></span></div>\ | |
<!-- /ko -->\ | |
<div class="tagbox-item empty" data-bind="visible: $component.suggestions().length === 0">Nothing found</div>\ | |
</div>\ | |
</div>' | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
usage:
js
document.createElement('component-kotag');
html