mirror of
https://github.com/go-gitea/gitea.git
synced 2025-04-22 19:58:24 +02:00
We would never update or build fomantic again, we have forked it as a private library long time ago. So just put the JS and CSS files in "fomantic/build" into git. And use "import" to use them. Remove "form.js", rewrite "tab" component. All source code is from official Fomantic UI build. Will apply patches in separate PRs.
1566 lines
52 KiB
JavaScript
1566 lines
52 KiB
JavaScript
/*!
|
|
* # Fomantic-UI - Search
|
|
* http://github.com/fomantic/Fomantic-UI/
|
|
*
|
|
*
|
|
* Released under the MIT license
|
|
* http://opensource.org/licenses/MIT
|
|
*
|
|
*/
|
|
|
|
;(function ($, window, document, undefined) {
|
|
|
|
'use strict';
|
|
|
|
$.isFunction = $.isFunction || function(obj) {
|
|
return typeof obj === "function" && typeof obj.nodeType !== "number";
|
|
};
|
|
|
|
window = (typeof window != 'undefined' && window.Math == Math)
|
|
? window
|
|
: (typeof self != 'undefined' && self.Math == Math)
|
|
? self
|
|
: Function('return this')()
|
|
;
|
|
|
|
$.fn.search = function(parameters) {
|
|
var
|
|
$allModules = $(this),
|
|
moduleSelector = $allModules.selector || '',
|
|
|
|
time = new Date().getTime(),
|
|
performance = [],
|
|
|
|
query = arguments[0],
|
|
methodInvoked = (typeof query == 'string'),
|
|
queryArguments = [].slice.call(arguments, 1),
|
|
returnedValue
|
|
;
|
|
$(this)
|
|
.each(function() {
|
|
var
|
|
settings = ( $.isPlainObject(parameters) )
|
|
? $.extend(true, {}, $.fn.search.settings, parameters)
|
|
: $.extend({}, $.fn.search.settings),
|
|
|
|
className = settings.className,
|
|
metadata = settings.metadata,
|
|
regExp = settings.regExp,
|
|
fields = settings.fields,
|
|
selector = settings.selector,
|
|
error = settings.error,
|
|
namespace = settings.namespace,
|
|
|
|
eventNamespace = '.' + namespace,
|
|
moduleNamespace = namespace + '-module',
|
|
|
|
$module = $(this),
|
|
$prompt = $module.find(selector.prompt),
|
|
$searchButton = $module.find(selector.searchButton),
|
|
$results = $module.find(selector.results),
|
|
$result = $module.find(selector.result),
|
|
$category = $module.find(selector.category),
|
|
|
|
element = this,
|
|
instance = $module.data(moduleNamespace),
|
|
|
|
disabledBubbled = false,
|
|
resultsDismissed = false,
|
|
|
|
module
|
|
;
|
|
|
|
module = {
|
|
|
|
initialize: function() {
|
|
module.verbose('Initializing module');
|
|
module.get.settings();
|
|
module.determine.searchFields();
|
|
module.bind.events();
|
|
module.set.type();
|
|
module.create.results();
|
|
module.instantiate();
|
|
},
|
|
instantiate: function() {
|
|
module.verbose('Storing instance of module', module);
|
|
instance = module;
|
|
$module
|
|
.data(moduleNamespace, module)
|
|
;
|
|
},
|
|
destroy: function() {
|
|
module.verbose('Destroying instance');
|
|
$module
|
|
.off(eventNamespace)
|
|
.removeData(moduleNamespace)
|
|
;
|
|
},
|
|
|
|
refresh: function() {
|
|
module.debug('Refreshing selector cache');
|
|
$prompt = $module.find(selector.prompt);
|
|
$searchButton = $module.find(selector.searchButton);
|
|
$category = $module.find(selector.category);
|
|
$results = $module.find(selector.results);
|
|
$result = $module.find(selector.result);
|
|
},
|
|
|
|
refreshResults: function() {
|
|
$results = $module.find(selector.results);
|
|
$result = $module.find(selector.result);
|
|
},
|
|
|
|
bind: {
|
|
events: function() {
|
|
module.verbose('Binding events to search');
|
|
if(settings.automatic) {
|
|
$module
|
|
.on(module.get.inputEvent() + eventNamespace, selector.prompt, module.event.input)
|
|
;
|
|
$prompt
|
|
.attr('autocomplete', 'off')
|
|
;
|
|
}
|
|
$module
|
|
// prompt
|
|
.on('focus' + eventNamespace, selector.prompt, module.event.focus)
|
|
.on('blur' + eventNamespace, selector.prompt, module.event.blur)
|
|
.on('keydown' + eventNamespace, selector.prompt, module.handleKeyboard)
|
|
// search button
|
|
.on('click' + eventNamespace, selector.searchButton, module.query)
|
|
// results
|
|
.on('mousedown' + eventNamespace, selector.results, module.event.result.mousedown)
|
|
.on('mouseup' + eventNamespace, selector.results, module.event.result.mouseup)
|
|
.on('click' + eventNamespace, selector.result, module.event.result.click)
|
|
;
|
|
}
|
|
},
|
|
|
|
determine: {
|
|
searchFields: function() {
|
|
// this makes sure $.extend does not add specified search fields to default fields
|
|
// this is the only setting which should not extend defaults
|
|
if(parameters && parameters.searchFields !== undefined) {
|
|
settings.searchFields = parameters.searchFields;
|
|
}
|
|
}
|
|
},
|
|
|
|
event: {
|
|
input: function() {
|
|
if(settings.searchDelay) {
|
|
clearTimeout(module.timer);
|
|
module.timer = setTimeout(function() {
|
|
if(module.is.focused()) {
|
|
module.query();
|
|
}
|
|
}, settings.searchDelay);
|
|
}
|
|
else {
|
|
module.query();
|
|
}
|
|
},
|
|
focus: function() {
|
|
module.set.focus();
|
|
if(settings.searchOnFocus && module.has.minimumCharacters() ) {
|
|
module.query(function() {
|
|
if(module.can.show() ) {
|
|
module.showResults();
|
|
}
|
|
});
|
|
}
|
|
},
|
|
blur: function(event) {
|
|
var
|
|
pageLostFocus = (document.activeElement === this),
|
|
callback = function() {
|
|
module.cancel.query();
|
|
module.remove.focus();
|
|
module.timer = setTimeout(module.hideResults, settings.hideDelay);
|
|
}
|
|
;
|
|
if(pageLostFocus) {
|
|
return;
|
|
}
|
|
resultsDismissed = false;
|
|
if(module.resultsClicked) {
|
|
module.debug('Determining if user action caused search to close');
|
|
$module
|
|
.one('click.close' + eventNamespace, selector.results, function(event) {
|
|
if(module.is.inMessage(event) || disabledBubbled) {
|
|
$prompt.focus();
|
|
return;
|
|
}
|
|
disabledBubbled = false;
|
|
if( !module.is.animating() && !module.is.hidden()) {
|
|
callback();
|
|
}
|
|
})
|
|
;
|
|
}
|
|
else {
|
|
module.debug('Input blurred without user action, closing results');
|
|
callback();
|
|
}
|
|
},
|
|
result: {
|
|
mousedown: function() {
|
|
module.resultsClicked = true;
|
|
},
|
|
mouseup: function() {
|
|
module.resultsClicked = false;
|
|
},
|
|
click: function(event) {
|
|
module.debug('Search result selected');
|
|
var
|
|
$result = $(this),
|
|
$title = $result.find(selector.title).eq(0),
|
|
$link = $result.is('a[href]')
|
|
? $result
|
|
: $result.find('a[href]').eq(0),
|
|
href = $link.attr('href') || false,
|
|
target = $link.attr('target') || false,
|
|
// title is used for result lookup
|
|
value = ($title.length > 0)
|
|
? $title.text()
|
|
: false,
|
|
results = module.get.results(),
|
|
result = $result.data(metadata.result) || module.get.result(value, results)
|
|
;
|
|
if(value) {
|
|
module.set.value(value);
|
|
}
|
|
if( $.isFunction(settings.onSelect) ) {
|
|
if(settings.onSelect.call(element, result, results) === false) {
|
|
module.debug('Custom onSelect callback cancelled default select action');
|
|
disabledBubbled = true;
|
|
return;
|
|
}
|
|
}
|
|
module.hideResults();
|
|
if(href) {
|
|
event.preventDefault();
|
|
module.verbose('Opening search link found in result', $link);
|
|
if(target == '_blank' || event.ctrlKey) {
|
|
window.open(href);
|
|
}
|
|
else {
|
|
window.location.href = (href);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
ensureVisible: function ensureVisible($el) {
|
|
var elTop, elBottom, resultsScrollTop, resultsHeight;
|
|
|
|
elTop = $el.position().top;
|
|
elBottom = elTop + $el.outerHeight(true);
|
|
|
|
resultsScrollTop = $results.scrollTop();
|
|
resultsHeight = $results.height()
|
|
parseInt($results.css('paddingTop'), 0) +
|
|
parseInt($results.css('paddingBottom'), 0);
|
|
|
|
if (elTop < 0) {
|
|
$results.scrollTop(resultsScrollTop + elTop);
|
|
}
|
|
|
|
else if (resultsHeight < elBottom) {
|
|
$results.scrollTop(resultsScrollTop + (elBottom - resultsHeight));
|
|
}
|
|
},
|
|
handleKeyboard: function(event) {
|
|
var
|
|
// force selector refresh
|
|
$result = $module.find(selector.result),
|
|
$category = $module.find(selector.category),
|
|
$activeResult = $result.filter('.' + className.active),
|
|
currentIndex = $result.index( $activeResult ),
|
|
resultSize = $result.length,
|
|
hasActiveResult = $activeResult.length > 0,
|
|
|
|
keyCode = event.which,
|
|
keys = {
|
|
backspace : 8,
|
|
enter : 13,
|
|
escape : 27,
|
|
upArrow : 38,
|
|
downArrow : 40
|
|
},
|
|
newIndex
|
|
;
|
|
// search shortcuts
|
|
if(keyCode == keys.escape) {
|
|
module.verbose('Escape key pressed, blurring search field');
|
|
module.hideResults();
|
|
resultsDismissed = true;
|
|
}
|
|
if( module.is.visible() ) {
|
|
if(keyCode == keys.enter) {
|
|
module.verbose('Enter key pressed, selecting active result');
|
|
if( $result.filter('.' + className.active).length > 0 ) {
|
|
module.event.result.click.call($result.filter('.' + className.active), event);
|
|
event.preventDefault();
|
|
return false;
|
|
}
|
|
}
|
|
else if(keyCode == keys.upArrow && hasActiveResult) {
|
|
module.verbose('Up key pressed, changing active result');
|
|
newIndex = (currentIndex - 1 < 0)
|
|
? currentIndex
|
|
: currentIndex - 1
|
|
;
|
|
$category
|
|
.removeClass(className.active)
|
|
;
|
|
$result
|
|
.removeClass(className.active)
|
|
.eq(newIndex)
|
|
.addClass(className.active)
|
|
.closest($category)
|
|
.addClass(className.active)
|
|
;
|
|
module.ensureVisible($result.eq(newIndex));
|
|
event.preventDefault();
|
|
}
|
|
else if(keyCode == keys.downArrow) {
|
|
module.verbose('Down key pressed, changing active result');
|
|
newIndex = (currentIndex + 1 >= resultSize)
|
|
? currentIndex
|
|
: currentIndex + 1
|
|
;
|
|
$category
|
|
.removeClass(className.active)
|
|
;
|
|
$result
|
|
.removeClass(className.active)
|
|
.eq(newIndex)
|
|
.addClass(className.active)
|
|
.closest($category)
|
|
.addClass(className.active)
|
|
;
|
|
module.ensureVisible($result.eq(newIndex));
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
else {
|
|
// query shortcuts
|
|
if(keyCode == keys.enter) {
|
|
module.verbose('Enter key pressed, executing query');
|
|
module.query();
|
|
module.set.buttonPressed();
|
|
$prompt.one('keyup', module.remove.buttonFocus);
|
|
}
|
|
}
|
|
},
|
|
|
|
setup: {
|
|
api: function(searchTerm, callback) {
|
|
var
|
|
apiSettings = {
|
|
debug : settings.debug,
|
|
on : false,
|
|
cache : settings.cache,
|
|
action : 'search',
|
|
urlData : {
|
|
query : searchTerm
|
|
},
|
|
onSuccess : function(response) {
|
|
module.parse.response.call(element, response, searchTerm);
|
|
callback();
|
|
},
|
|
onFailure : function() {
|
|
module.displayMessage(error.serverError);
|
|
callback();
|
|
},
|
|
onAbort : function(response) {
|
|
},
|
|
onError : module.error
|
|
}
|
|
;
|
|
$.extend(true, apiSettings, settings.apiSettings);
|
|
module.verbose('Setting up API request', apiSettings);
|
|
$module.api(apiSettings);
|
|
}
|
|
},
|
|
|
|
can: {
|
|
useAPI: function() {
|
|
return $.fn.api !== undefined;
|
|
},
|
|
show: function() {
|
|
return module.is.focused() && !module.is.visible() && !module.is.empty();
|
|
},
|
|
transition: function() {
|
|
return settings.transition && $.fn.transition !== undefined && $module.transition('is supported');
|
|
}
|
|
},
|
|
|
|
is: {
|
|
animating: function() {
|
|
return $results.hasClass(className.animating);
|
|
},
|
|
hidden: function() {
|
|
return $results.hasClass(className.hidden);
|
|
},
|
|
inMessage: function(event) {
|
|
if(!event.target) {
|
|
return;
|
|
}
|
|
var
|
|
$target = $(event.target),
|
|
isInDOM = $.contains(document.documentElement, event.target)
|
|
;
|
|
return (isInDOM && $target.closest(selector.message).length > 0);
|
|
},
|
|
empty: function() {
|
|
return ($results.html() === '');
|
|
},
|
|
visible: function() {
|
|
return ($results.filter(':visible').length > 0);
|
|
},
|
|
focused: function() {
|
|
return ($prompt.filter(':focus').length > 0);
|
|
}
|
|
},
|
|
|
|
get: {
|
|
settings: function() {
|
|
if($.isPlainObject(parameters) && parameters.searchFullText) {
|
|
settings.fullTextSearch = parameters.searchFullText;
|
|
module.error(settings.error.oldSearchSyntax, element);
|
|
}
|
|
if (settings.ignoreDiacritics && !String.prototype.normalize) {
|
|
settings.ignoreDiacritics = false;
|
|
module.error(error.noNormalize, element);
|
|
}
|
|
},
|
|
inputEvent: function() {
|
|
var
|
|
prompt = $prompt[0],
|
|
inputEvent = (prompt !== undefined && prompt.oninput !== undefined)
|
|
? 'input'
|
|
: (prompt !== undefined && prompt.onpropertychange !== undefined)
|
|
? 'propertychange'
|
|
: 'keyup'
|
|
;
|
|
return inputEvent;
|
|
},
|
|
value: function() {
|
|
return $prompt.val();
|
|
},
|
|
results: function() {
|
|
var
|
|
results = $module.data(metadata.results)
|
|
;
|
|
return results;
|
|
},
|
|
result: function(value, results) {
|
|
var
|
|
result = false
|
|
;
|
|
value = (value !== undefined)
|
|
? value
|
|
: module.get.value()
|
|
;
|
|
results = (results !== undefined)
|
|
? results
|
|
: module.get.results()
|
|
;
|
|
if(settings.type === 'category') {
|
|
module.debug('Finding result that matches', value);
|
|
$.each(results, function(index, category) {
|
|
if(Array.isArray(category.results)) {
|
|
result = module.search.object(value, category.results)[0];
|
|
// don't continue searching if a result is found
|
|
if(result) {
|
|
return false;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
module.debug('Finding result in results object', value);
|
|
result = module.search.object(value, results)[0];
|
|
}
|
|
return result || false;
|
|
},
|
|
},
|
|
|
|
select: {
|
|
firstResult: function() {
|
|
module.verbose('Selecting first result');
|
|
$result.first().addClass(className.active);
|
|
}
|
|
},
|
|
|
|
set: {
|
|
focus: function() {
|
|
$module.addClass(className.focus);
|
|
},
|
|
loading: function() {
|
|
$module.addClass(className.loading);
|
|
},
|
|
value: function(value) {
|
|
module.verbose('Setting search input value', value);
|
|
$prompt
|
|
.val(value)
|
|
;
|
|
},
|
|
type: function(type) {
|
|
type = type || settings.type;
|
|
if(settings.type == 'category') {
|
|
$module.addClass(settings.type);
|
|
}
|
|
},
|
|
buttonPressed: function() {
|
|
$searchButton.addClass(className.pressed);
|
|
}
|
|
},
|
|
|
|
remove: {
|
|
loading: function() {
|
|
$module.removeClass(className.loading);
|
|
},
|
|
focus: function() {
|
|
$module.removeClass(className.focus);
|
|
},
|
|
buttonPressed: function() {
|
|
$searchButton.removeClass(className.pressed);
|
|
},
|
|
diacritics: function(text) {
|
|
return settings.ignoreDiacritics ? text.normalize('NFD').replace(/[\u0300-\u036f]/g, '') : text;
|
|
}
|
|
},
|
|
|
|
query: function(callback) {
|
|
callback = $.isFunction(callback)
|
|
? callback
|
|
: function(){}
|
|
;
|
|
var
|
|
searchTerm = module.get.value(),
|
|
cache = module.read.cache(searchTerm)
|
|
;
|
|
callback = callback || function() {};
|
|
if( module.has.minimumCharacters() ) {
|
|
if(cache) {
|
|
module.debug('Reading result from cache', searchTerm);
|
|
module.save.results(cache.results);
|
|
module.addResults(cache.html);
|
|
module.inject.id(cache.results);
|
|
callback();
|
|
}
|
|
else {
|
|
module.debug('Querying for', searchTerm);
|
|
if($.isPlainObject(settings.source) || Array.isArray(settings.source)) {
|
|
module.search.local(searchTerm);
|
|
callback();
|
|
}
|
|
else if( module.can.useAPI() ) {
|
|
module.search.remote(searchTerm, callback);
|
|
}
|
|
else {
|
|
module.error(error.source);
|
|
callback();
|
|
}
|
|
}
|
|
settings.onSearchQuery.call(element, searchTerm);
|
|
}
|
|
else {
|
|
module.hideResults();
|
|
}
|
|
},
|
|
|
|
search: {
|
|
local: function(searchTerm) {
|
|
var
|
|
results = module.search.object(searchTerm, settings.source),
|
|
searchHTML
|
|
;
|
|
module.set.loading();
|
|
module.save.results(results);
|
|
module.debug('Returned full local search results', results);
|
|
if(settings.maxResults > 0) {
|
|
module.debug('Using specified max results', results);
|
|
results = results.slice(0, settings.maxResults);
|
|
}
|
|
if(settings.type == 'category') {
|
|
results = module.create.categoryResults(results);
|
|
}
|
|
searchHTML = module.generateResults({
|
|
results: results
|
|
});
|
|
module.remove.loading();
|
|
module.addResults(searchHTML);
|
|
module.inject.id(results);
|
|
module.write.cache(searchTerm, {
|
|
html : searchHTML,
|
|
results : results
|
|
});
|
|
},
|
|
remote: function(searchTerm, callback) {
|
|
callback = $.isFunction(callback)
|
|
? callback
|
|
: function(){}
|
|
;
|
|
if($module.api('is loading')) {
|
|
$module.api('abort');
|
|
}
|
|
module.setup.api(searchTerm, callback);
|
|
$module
|
|
.api('query')
|
|
;
|
|
},
|
|
object: function(searchTerm, source, searchFields) {
|
|
searchTerm = module.remove.diacritics(String(searchTerm));
|
|
var
|
|
results = [],
|
|
exactResults = [],
|
|
fuzzyResults = [],
|
|
searchExp = searchTerm.replace(regExp.escape, '\\$&'),
|
|
matchRegExp = new RegExp(regExp.beginsWith + searchExp, 'i'),
|
|
|
|
// avoid duplicates when pushing results
|
|
addResult = function(array, result) {
|
|
var
|
|
notResult = ($.inArray(result, results) == -1),
|
|
notFuzzyResult = ($.inArray(result, fuzzyResults) == -1),
|
|
notExactResults = ($.inArray(result, exactResults) == -1)
|
|
;
|
|
if(notResult && notFuzzyResult && notExactResults) {
|
|
array.push(result);
|
|
}
|
|
}
|
|
;
|
|
source = source || settings.source;
|
|
searchFields = (searchFields !== undefined)
|
|
? searchFields
|
|
: settings.searchFields
|
|
;
|
|
|
|
// search fields should be array to loop correctly
|
|
if(!Array.isArray(searchFields)) {
|
|
searchFields = [searchFields];
|
|
}
|
|
|
|
// exit conditions if no source
|
|
if(source === undefined || source === false) {
|
|
module.error(error.source);
|
|
return [];
|
|
}
|
|
// iterate through search fields looking for matches
|
|
$.each(searchFields, function(index, field) {
|
|
$.each(source, function(label, content) {
|
|
var
|
|
fieldExists = (typeof content[field] == 'string') || (typeof content[field] == 'number')
|
|
;
|
|
if(fieldExists) {
|
|
var text;
|
|
if (typeof content[field] === 'string'){
|
|
text = module.remove.diacritics(content[field]);
|
|
} else {
|
|
text = content[field].toString();
|
|
}
|
|
if( text.search(matchRegExp) !== -1) {
|
|
// content starts with value (first in results)
|
|
addResult(results, content);
|
|
}
|
|
else if(settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, text) ) {
|
|
// content fuzzy matches (last in results)
|
|
addResult(exactResults, content);
|
|
}
|
|
else if(settings.fullTextSearch == true && module.fuzzySearch(searchTerm, text) ) {
|
|
// content fuzzy matches (last in results)
|
|
addResult(fuzzyResults, content);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
$.merge(exactResults, fuzzyResults);
|
|
$.merge(results, exactResults);
|
|
return results;
|
|
}
|
|
},
|
|
exactSearch: function (query, term) {
|
|
query = query.toLowerCase();
|
|
term = term.toLowerCase();
|
|
return term.indexOf(query) > -1;
|
|
},
|
|
fuzzySearch: function(query, term) {
|
|
var
|
|
termLength = term.length,
|
|
queryLength = query.length
|
|
;
|
|
if(typeof query !== 'string') {
|
|
return false;
|
|
}
|
|
query = query.toLowerCase();
|
|
term = term.toLowerCase();
|
|
if(queryLength > termLength) {
|
|
return false;
|
|
}
|
|
if(queryLength === termLength) {
|
|
return (query === term);
|
|
}
|
|
search: for (var characterIndex = 0, nextCharacterIndex = 0; characterIndex < queryLength; characterIndex++) {
|
|
var
|
|
queryCharacter = query.charCodeAt(characterIndex)
|
|
;
|
|
while(nextCharacterIndex < termLength) {
|
|
if(term.charCodeAt(nextCharacterIndex++) === queryCharacter) {
|
|
continue search;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
parse: {
|
|
response: function(response, searchTerm) {
|
|
if(Array.isArray(response)){
|
|
var o={};
|
|
o[fields.results]=response;
|
|
response = o;
|
|
}
|
|
var
|
|
searchHTML = module.generateResults(response)
|
|
;
|
|
module.verbose('Parsing server response', response);
|
|
if(response !== undefined) {
|
|
if(searchTerm !== undefined && response[fields.results] !== undefined) {
|
|
module.addResults(searchHTML);
|
|
module.inject.id(response[fields.results]);
|
|
module.write.cache(searchTerm, {
|
|
html : searchHTML,
|
|
results : response[fields.results]
|
|
});
|
|
module.save.results(response[fields.results]);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
cancel: {
|
|
query: function() {
|
|
if( module.can.useAPI() ) {
|
|
$module.api('abort');
|
|
}
|
|
}
|
|
},
|
|
|
|
has: {
|
|
minimumCharacters: function() {
|
|
var
|
|
searchTerm = module.get.value(),
|
|
numCharacters = searchTerm.length
|
|
;
|
|
return (numCharacters >= settings.minCharacters);
|
|
},
|
|
results: function() {
|
|
if($results.length === 0) {
|
|
return false;
|
|
}
|
|
var
|
|
html = $results.html()
|
|
;
|
|
return html != '';
|
|
}
|
|
},
|
|
|
|
clear: {
|
|
cache: function(value) {
|
|
var
|
|
cache = $module.data(metadata.cache)
|
|
;
|
|
if(!value) {
|
|
module.debug('Clearing cache', value);
|
|
$module.removeData(metadata.cache);
|
|
}
|
|
else if(value && cache && cache[value]) {
|
|
module.debug('Removing value from cache', value);
|
|
delete cache[value];
|
|
$module.data(metadata.cache, cache);
|
|
}
|
|
}
|
|
},
|
|
|
|
read: {
|
|
cache: function(name) {
|
|
var
|
|
cache = $module.data(metadata.cache)
|
|
;
|
|
if(settings.cache) {
|
|
module.verbose('Checking cache for generated html for query', name);
|
|
return (typeof cache == 'object') && (cache[name] !== undefined)
|
|
? cache[name]
|
|
: false
|
|
;
|
|
}
|
|
return false;
|
|
}
|
|
},
|
|
|
|
create: {
|
|
categoryResults: function(results) {
|
|
var
|
|
categoryResults = {}
|
|
;
|
|
$.each(results, function(index, result) {
|
|
if(!result.category) {
|
|
return;
|
|
}
|
|
if(categoryResults[result.category] === undefined) {
|
|
module.verbose('Creating new category of results', result.category);
|
|
categoryResults[result.category] = {
|
|
name : result.category,
|
|
results : [result]
|
|
};
|
|
}
|
|
else {
|
|
categoryResults[result.category].results.push(result);
|
|
}
|
|
});
|
|
return categoryResults;
|
|
},
|
|
id: function(resultIndex, categoryIndex) {
|
|
var
|
|
resultID = (resultIndex + 1), // not zero indexed
|
|
letterID,
|
|
id
|
|
;
|
|
if(categoryIndex !== undefined) {
|
|
// start char code for "A"
|
|
letterID = String.fromCharCode(97 + categoryIndex);
|
|
id = letterID + resultID;
|
|
module.verbose('Creating category result id', id);
|
|
}
|
|
else {
|
|
id = resultID;
|
|
module.verbose('Creating result id', id);
|
|
}
|
|
return id;
|
|
},
|
|
results: function() {
|
|
if($results.length === 0) {
|
|
$results = $('<div />')
|
|
.addClass(className.results)
|
|
.appendTo($module)
|
|
;
|
|
}
|
|
}
|
|
},
|
|
|
|
inject: {
|
|
result: function(result, resultIndex, categoryIndex) {
|
|
module.verbose('Injecting result into results');
|
|
var
|
|
$selectedResult = (categoryIndex !== undefined)
|
|
? $results
|
|
.children().eq(categoryIndex)
|
|
.children(selector.results)
|
|
.first()
|
|
.children(selector.result)
|
|
.eq(resultIndex)
|
|
: $results
|
|
.children(selector.result).eq(resultIndex)
|
|
;
|
|
module.verbose('Injecting results metadata', $selectedResult);
|
|
$selectedResult
|
|
.data(metadata.result, result)
|
|
;
|
|
},
|
|
id: function(results) {
|
|
module.debug('Injecting unique ids into results');
|
|
var
|
|
// since results may be object, we must use counters
|
|
categoryIndex = 0,
|
|
resultIndex = 0
|
|
;
|
|
if(settings.type === 'category') {
|
|
// iterate through each category result
|
|
$.each(results, function(index, category) {
|
|
if(category.results.length > 0){
|
|
resultIndex = 0;
|
|
$.each(category.results, function(index, result) {
|
|
if(result.id === undefined) {
|
|
result.id = module.create.id(resultIndex, categoryIndex);
|
|
}
|
|
module.inject.result(result, resultIndex, categoryIndex);
|
|
resultIndex++;
|
|
});
|
|
categoryIndex++;
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
// top level
|
|
$.each(results, function(index, result) {
|
|
if(result.id === undefined) {
|
|
result.id = module.create.id(resultIndex);
|
|
}
|
|
module.inject.result(result, resultIndex);
|
|
resultIndex++;
|
|
});
|
|
}
|
|
return results;
|
|
}
|
|
},
|
|
|
|
save: {
|
|
results: function(results) {
|
|
module.verbose('Saving current search results to metadata', results);
|
|
$module.data(metadata.results, results);
|
|
}
|
|
},
|
|
|
|
write: {
|
|
cache: function(name, value) {
|
|
var
|
|
cache = ($module.data(metadata.cache) !== undefined)
|
|
? $module.data(metadata.cache)
|
|
: {}
|
|
;
|
|
if(settings.cache) {
|
|
module.verbose('Writing generated html to cache', name, value);
|
|
cache[name] = value;
|
|
$module
|
|
.data(metadata.cache, cache)
|
|
;
|
|
}
|
|
}
|
|
},
|
|
|
|
addResults: function(html) {
|
|
if( $.isFunction(settings.onResultsAdd) ) {
|
|
if( settings.onResultsAdd.call($results, html) === false ) {
|
|
module.debug('onResultsAdd callback cancelled default action');
|
|
return false;
|
|
}
|
|
}
|
|
if(html) {
|
|
$results
|
|
.html(html)
|
|
;
|
|
module.refreshResults();
|
|
if(settings.selectFirstResult) {
|
|
module.select.firstResult();
|
|
}
|
|
module.showResults();
|
|
}
|
|
else {
|
|
module.hideResults(function() {
|
|
$results.empty();
|
|
});
|
|
}
|
|
},
|
|
|
|
showResults: function(callback) {
|
|
callback = $.isFunction(callback)
|
|
? callback
|
|
: function(){}
|
|
;
|
|
if(resultsDismissed) {
|
|
return;
|
|
}
|
|
if(!module.is.visible() && module.has.results()) {
|
|
if( module.can.transition() ) {
|
|
module.debug('Showing results with css animations');
|
|
$results
|
|
.transition({
|
|
animation : settings.transition + ' in',
|
|
debug : settings.debug,
|
|
verbose : settings.verbose,
|
|
duration : settings.duration,
|
|
onShow : function() {
|
|
var $firstResult = $module.find(selector.result).eq(0);
|
|
if($firstResult.length > 0) {
|
|
module.ensureVisible($firstResult);
|
|
}
|
|
},
|
|
onComplete : function() {
|
|
callback();
|
|
},
|
|
queue : true
|
|
})
|
|
;
|
|
}
|
|
else {
|
|
module.debug('Showing results with javascript');
|
|
$results
|
|
.stop()
|
|
.fadeIn(settings.duration, settings.easing)
|
|
;
|
|
}
|
|
settings.onResultsOpen.call($results);
|
|
}
|
|
},
|
|
hideResults: function(callback) {
|
|
callback = $.isFunction(callback)
|
|
? callback
|
|
: function(){}
|
|
;
|
|
if( module.is.visible() ) {
|
|
if( module.can.transition() ) {
|
|
module.debug('Hiding results with css animations');
|
|
$results
|
|
.transition({
|
|
animation : settings.transition + ' out',
|
|
debug : settings.debug,
|
|
verbose : settings.verbose,
|
|
duration : settings.duration,
|
|
onComplete : function() {
|
|
callback();
|
|
},
|
|
queue : true
|
|
})
|
|
;
|
|
}
|
|
else {
|
|
module.debug('Hiding results with javascript');
|
|
$results
|
|
.stop()
|
|
.fadeOut(settings.duration, settings.easing)
|
|
;
|
|
}
|
|
settings.onResultsClose.call($results);
|
|
}
|
|
},
|
|
|
|
generateResults: function(response) {
|
|
module.debug('Generating html from response', response);
|
|
var
|
|
template = settings.templates[settings.type],
|
|
isProperObject = ($.isPlainObject(response[fields.results]) && !$.isEmptyObject(response[fields.results])),
|
|
isProperArray = (Array.isArray(response[fields.results]) && response[fields.results].length > 0),
|
|
html = ''
|
|
;
|
|
if(isProperObject || isProperArray ) {
|
|
if(settings.maxResults > 0) {
|
|
if(isProperObject) {
|
|
if(settings.type == 'standard') {
|
|
module.error(error.maxResults);
|
|
}
|
|
}
|
|
else {
|
|
response[fields.results] = response[fields.results].slice(0, settings.maxResults);
|
|
}
|
|
}
|
|
if($.isFunction(template)) {
|
|
html = template(response, fields, settings.preserveHTML);
|
|
}
|
|
else {
|
|
module.error(error.noTemplate, false);
|
|
}
|
|
}
|
|
else if(settings.showNoResults) {
|
|
html = module.displayMessage(error.noResults, 'empty', error.noResultsHeader);
|
|
}
|
|
settings.onResults.call(element, response);
|
|
return html;
|
|
},
|
|
|
|
displayMessage: function(text, type, header) {
|
|
type = type || 'standard';
|
|
module.debug('Displaying message', text, type, header);
|
|
module.addResults( settings.templates.message(text, type, header) );
|
|
return settings.templates.message(text, type, header);
|
|
},
|
|
|
|
setting: function(name, value) {
|
|
if( $.isPlainObject(name) ) {
|
|
$.extend(true, settings, name);
|
|
}
|
|
else if(value !== undefined) {
|
|
settings[name] = value;
|
|
}
|
|
else {
|
|
return settings[name];
|
|
}
|
|
},
|
|
internal: function(name, value) {
|
|
if( $.isPlainObject(name) ) {
|
|
$.extend(true, module, name);
|
|
}
|
|
else if(value !== undefined) {
|
|
module[name] = value;
|
|
}
|
|
else {
|
|
return module[name];
|
|
}
|
|
},
|
|
debug: function() {
|
|
if(!settings.silent && settings.debug) {
|
|
if(settings.performance) {
|
|
module.performance.log(arguments);
|
|
}
|
|
else {
|
|
module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
|
|
module.debug.apply(console, arguments);
|
|
}
|
|
}
|
|
},
|
|
verbose: function() {
|
|
if(!settings.silent && settings.verbose && settings.debug) {
|
|
if(settings.performance) {
|
|
module.performance.log(arguments);
|
|
}
|
|
else {
|
|
module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
|
|
module.verbose.apply(console, arguments);
|
|
}
|
|
}
|
|
},
|
|
error: function() {
|
|
if(!settings.silent) {
|
|
module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
|
|
module.error.apply(console, arguments);
|
|
}
|
|
},
|
|
performance: {
|
|
log: function(message) {
|
|
var
|
|
currentTime,
|
|
executionTime,
|
|
previousTime
|
|
;
|
|
if(settings.performance) {
|
|
currentTime = new Date().getTime();
|
|
previousTime = time || currentTime;
|
|
executionTime = currentTime - previousTime;
|
|
time = currentTime;
|
|
performance.push({
|
|
'Name' : message[0],
|
|
'Arguments' : [].slice.call(message, 1) || '',
|
|
'Element' : element,
|
|
'Execution Time' : executionTime
|
|
});
|
|
}
|
|
clearTimeout(module.performance.timer);
|
|
module.performance.timer = setTimeout(module.performance.display, 500);
|
|
},
|
|
display: function() {
|
|
var
|
|
title = settings.name + ':',
|
|
totalTime = 0
|
|
;
|
|
time = false;
|
|
clearTimeout(module.performance.timer);
|
|
$.each(performance, function(index, data) {
|
|
totalTime += data['Execution Time'];
|
|
});
|
|
title += ' ' + totalTime + 'ms';
|
|
if(moduleSelector) {
|
|
title += ' \'' + moduleSelector + '\'';
|
|
}
|
|
if($allModules.length > 1) {
|
|
title += ' ' + '(' + $allModules.length + ')';
|
|
}
|
|
if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
|
|
console.groupCollapsed(title);
|
|
if(console.table) {
|
|
console.table(performance);
|
|
}
|
|
else {
|
|
$.each(performance, function(index, data) {
|
|
console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
|
|
});
|
|
}
|
|
console.groupEnd();
|
|
}
|
|
performance = [];
|
|
}
|
|
},
|
|
invoke: function(query, passedArguments, context) {
|
|
var
|
|
object = instance,
|
|
maxDepth,
|
|
found,
|
|
response
|
|
;
|
|
passedArguments = passedArguments || queryArguments;
|
|
context = element || context;
|
|
if(typeof query == 'string' && object !== undefined) {
|
|
query = query.split(/[\. ]/);
|
|
maxDepth = query.length - 1;
|
|
$.each(query, function(depth, value) {
|
|
var camelCaseValue = (depth != maxDepth)
|
|
? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
|
|
: query
|
|
;
|
|
if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
|
|
object = object[camelCaseValue];
|
|
}
|
|
else if( object[camelCaseValue] !== undefined ) {
|
|
found = object[camelCaseValue];
|
|
return false;
|
|
}
|
|
else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
|
|
object = object[value];
|
|
}
|
|
else if( object[value] !== undefined ) {
|
|
found = object[value];
|
|
return false;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
if( $.isFunction( found ) ) {
|
|
response = found.apply(context, passedArguments);
|
|
}
|
|
else if(found !== undefined) {
|
|
response = found;
|
|
}
|
|
if(Array.isArray(returnedValue)) {
|
|
returnedValue.push(response);
|
|
}
|
|
else if(returnedValue !== undefined) {
|
|
returnedValue = [returnedValue, response];
|
|
}
|
|
else if(response !== undefined) {
|
|
returnedValue = response;
|
|
}
|
|
return found;
|
|
}
|
|
};
|
|
if(methodInvoked) {
|
|
if(instance === undefined) {
|
|
module.initialize();
|
|
}
|
|
module.invoke(query);
|
|
}
|
|
else {
|
|
if(instance !== undefined) {
|
|
instance.invoke('destroy');
|
|
}
|
|
module.initialize();
|
|
}
|
|
|
|
})
|
|
;
|
|
|
|
return (returnedValue !== undefined)
|
|
? returnedValue
|
|
: this
|
|
;
|
|
};
|
|
|
|
$.fn.search.settings = {
|
|
|
|
name : 'Search',
|
|
namespace : 'search',
|
|
|
|
silent : false,
|
|
debug : false,
|
|
verbose : false,
|
|
performance : true,
|
|
|
|
// template to use (specified in settings.templates)
|
|
type : 'standard',
|
|
|
|
// minimum characters required to search
|
|
minCharacters : 1,
|
|
|
|
// whether to select first result after searching automatically
|
|
selectFirstResult : false,
|
|
|
|
// API config
|
|
apiSettings : false,
|
|
|
|
// object to search
|
|
source : false,
|
|
|
|
// Whether search should query current term on focus
|
|
searchOnFocus : true,
|
|
|
|
// fields to search
|
|
searchFields : [
|
|
'id',
|
|
'title',
|
|
'description'
|
|
],
|
|
|
|
// field to display in standard results template
|
|
displayField : '',
|
|
|
|
// search anywhere in value (set to 'exact' to require exact matches
|
|
fullTextSearch : 'exact',
|
|
|
|
// match results also if they contain diacritics of the same base character (for example searching for "a" will also match "á" or "â" or "à", etc...)
|
|
ignoreDiacritics : false,
|
|
|
|
// whether to add events to prompt automatically
|
|
automatic : true,
|
|
|
|
// delay before hiding menu after blur
|
|
hideDelay : 0,
|
|
|
|
// delay before searching
|
|
searchDelay : 200,
|
|
|
|
// maximum results returned from search
|
|
maxResults : 7,
|
|
|
|
// whether to store lookups in local cache
|
|
cache : true,
|
|
|
|
// whether no results errors should be shown
|
|
showNoResults : true,
|
|
|
|
// preserve possible html of resultset values
|
|
preserveHTML : true,
|
|
|
|
// transition settings
|
|
transition : 'scale',
|
|
duration : 200,
|
|
easing : 'easeOutExpo',
|
|
|
|
// callbacks
|
|
onSelect : false,
|
|
onResultsAdd : false,
|
|
|
|
onSearchQuery : function(query){},
|
|
onResults : function(response){},
|
|
|
|
onResultsOpen : function(){},
|
|
onResultsClose : function(){},
|
|
|
|
className: {
|
|
animating : 'animating',
|
|
active : 'active',
|
|
empty : 'empty',
|
|
focus : 'focus',
|
|
hidden : 'hidden',
|
|
loading : 'loading',
|
|
results : 'results',
|
|
pressed : 'down'
|
|
},
|
|
|
|
error : {
|
|
source : 'Cannot search. No source used, and Semantic API module was not included',
|
|
noResultsHeader : 'No Results',
|
|
noResults : 'Your search returned no results',
|
|
logging : 'Error in debug logging, exiting.',
|
|
noEndpoint : 'No search endpoint was specified',
|
|
noTemplate : 'A valid template name was not specified.',
|
|
oldSearchSyntax : 'searchFullText setting has been renamed fullTextSearch for consistency, please adjust your settings.',
|
|
serverError : 'There was an issue querying the server.',
|
|
maxResults : 'Results must be an array to use maxResults setting',
|
|
method : 'The method you called is not defined.',
|
|
noNormalize : '"ignoreDiacritics" setting will be ignored. Browser does not support String().normalize(). You may consider including <https://cdn.jsdelivr.net/npm/unorm@1.4.1/lib/unorm.min.js> as a polyfill.'
|
|
},
|
|
|
|
metadata: {
|
|
cache : 'cache',
|
|
results : 'results',
|
|
result : 'result'
|
|
},
|
|
|
|
regExp: {
|
|
escape : /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,
|
|
beginsWith : '(?:\s|^)'
|
|
},
|
|
|
|
// maps api response attributes to internal representation
|
|
fields: {
|
|
categories : 'results', // array of categories (category view)
|
|
categoryName : 'name', // name of category (category view)
|
|
categoryResults : 'results', // array of results (category view)
|
|
description : 'description', // result description
|
|
image : 'image', // result image
|
|
price : 'price', // result price
|
|
results : 'results', // array of results (standard)
|
|
title : 'title', // result title
|
|
url : 'url', // result url
|
|
action : 'action', // "view more" object name
|
|
actionText : 'text', // "view more" text
|
|
actionURL : 'url' // "view more" url
|
|
},
|
|
|
|
selector : {
|
|
prompt : '.prompt',
|
|
searchButton : '.search.button',
|
|
results : '.results',
|
|
message : '.results > .message',
|
|
category : '.category',
|
|
result : '.result',
|
|
title : '.title, .name'
|
|
},
|
|
|
|
templates: {
|
|
escape: function(string, preserveHTML) {
|
|
if (preserveHTML){
|
|
return string;
|
|
}
|
|
var
|
|
badChars = /[<>"'`]/g,
|
|
shouldEscape = /[&<>"'`]/,
|
|
escape = {
|
|
"<": "<",
|
|
">": ">",
|
|
'"': """,
|
|
"'": "'",
|
|
"`": "`"
|
|
},
|
|
escapedChar = function(chr) {
|
|
return escape[chr];
|
|
}
|
|
;
|
|
if(shouldEscape.test(string)) {
|
|
string = string.replace(/&(?![a-z0-9#]{1,6};)/, "&");
|
|
return string.replace(badChars, escapedChar);
|
|
}
|
|
return string;
|
|
},
|
|
message: function(message, type, header) {
|
|
var
|
|
html = ''
|
|
;
|
|
if(message !== undefined && type !== undefined) {
|
|
html += ''
|
|
+ '<div class="message ' + type + '">'
|
|
;
|
|
if(header) {
|
|
html += ''
|
|
+ '<div class="header">' + header + '</div>'
|
|
;
|
|
}
|
|
html += ' <div class="description">' + message + '</div>';
|
|
html += '</div>';
|
|
}
|
|
return html;
|
|
},
|
|
category: function(response, fields, preserveHTML) {
|
|
var
|
|
html = '',
|
|
escape = $.fn.search.settings.templates.escape
|
|
;
|
|
if(response[fields.categoryResults] !== undefined) {
|
|
|
|
// each category
|
|
$.each(response[fields.categoryResults], function(index, category) {
|
|
if(category[fields.results] !== undefined && category.results.length > 0) {
|
|
|
|
html += '<div class="category">';
|
|
|
|
if(category[fields.categoryName] !== undefined) {
|
|
html += '<div class="name">' + escape(category[fields.categoryName], preserveHTML) + '</div>';
|
|
}
|
|
|
|
// each item inside category
|
|
html += '<div class="results">';
|
|
$.each(category.results, function(index, result) {
|
|
if(result[fields.url]) {
|
|
html += '<a class="result" href="' + result[fields.url].replace(/"/g,"") + '">';
|
|
}
|
|
else {
|
|
html += '<a class="result">';
|
|
}
|
|
if(result[fields.image] !== undefined) {
|
|
html += ''
|
|
+ '<div class="image">'
|
|
+ ' <img src="' + result[fields.image].replace(/"/g,"") + '">'
|
|
+ '</div>'
|
|
;
|
|
}
|
|
html += '<div class="content">';
|
|
if(result[fields.price] !== undefined) {
|
|
html += '<div class="price">' + escape(result[fields.price], preserveHTML) + '</div>';
|
|
}
|
|
if(result[fields.title] !== undefined) {
|
|
html += '<div class="title">' + escape(result[fields.title], preserveHTML) + '</div>';
|
|
}
|
|
if(result[fields.description] !== undefined) {
|
|
html += '<div class="description">' + escape(result[fields.description], preserveHTML) + '</div>';
|
|
}
|
|
html += ''
|
|
+ '</div>'
|
|
;
|
|
html += '</a>';
|
|
});
|
|
html += '</div>';
|
|
html += ''
|
|
+ '</div>'
|
|
;
|
|
}
|
|
});
|
|
if(response[fields.action]) {
|
|
if(fields.actionURL === false) {
|
|
html += ''
|
|
+ '<div class="action">'
|
|
+ escape(response[fields.action][fields.actionText], preserveHTML)
|
|
+ '</div>';
|
|
} else {
|
|
html += ''
|
|
+ '<a href="' + response[fields.action][fields.actionURL].replace(/"/g,"") + '" class="action">'
|
|
+ escape(response[fields.action][fields.actionText], preserveHTML)
|
|
+ '</a>';
|
|
}
|
|
}
|
|
return html;
|
|
}
|
|
return false;
|
|
},
|
|
standard: function(response, fields, preserveHTML) {
|
|
var
|
|
html = '',
|
|
escape = $.fn.search.settings.templates.escape
|
|
;
|
|
if(response[fields.results] !== undefined) {
|
|
|
|
// each result
|
|
$.each(response[fields.results], function(index, result) {
|
|
if(result[fields.url]) {
|
|
html += '<a class="result" href="' + result[fields.url].replace(/"/g,"") + '">';
|
|
}
|
|
else {
|
|
html += '<a class="result">';
|
|
}
|
|
if(result[fields.image] !== undefined) {
|
|
html += ''
|
|
+ '<div class="image">'
|
|
+ ' <img src="' + result[fields.image].replace(/"/g,"") + '">'
|
|
+ '</div>'
|
|
;
|
|
}
|
|
html += '<div class="content">';
|
|
if(result[fields.price] !== undefined) {
|
|
html += '<div class="price">' + escape(result[fields.price], preserveHTML) + '</div>';
|
|
}
|
|
if(result[fields.title] !== undefined) {
|
|
html += '<div class="title">' + escape(result[fields.title], preserveHTML) + '</div>';
|
|
}
|
|
if(result[fields.description] !== undefined) {
|
|
html += '<div class="description">' + escape(result[fields.description], preserveHTML) + '</div>';
|
|
}
|
|
html += ''
|
|
+ '</div>'
|
|
;
|
|
html += '</a>';
|
|
});
|
|
if(response[fields.action]) {
|
|
if(fields.actionURL === false) {
|
|
html += ''
|
|
+ '<div class="action">'
|
|
+ escape(response[fields.action][fields.actionText], preserveHTML)
|
|
+ '</div>';
|
|
} else {
|
|
html += ''
|
|
+ '<a href="' + response[fields.action][fields.actionURL].replace(/"/g,"") + '" class="action">'
|
|
+ escape(response[fields.action][fields.actionText], preserveHTML)
|
|
+ '</a>';
|
|
}
|
|
}
|
|
return html;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
|
|
})( jQuery, window, document );
|