2022-01-13 13:00:31 +00:00
/ * !
2023-12-19 14:36:33 +00:00
* # Fomantic - UI 2.9 . 3 - Search
2023-08-17 09:47:40 +00:00
* https : //github.com/fomantic/Fomantic-UI/
2022-01-13 13:00:31 +00:00
*
*
* Released under the MIT license
2023-08-17 09:47:40 +00:00
* https : //opensource.org/licenses/MIT
2022-01-13 13:00:31 +00:00
*
* /
2023-08-17 09:47:40 +00:00
( function ( $ , window , document ) {
'use strict' ;
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
function isFunction ( obj ) {
return typeof obj === 'function' && typeof obj . nodeType !== 'number' ;
}
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
window = window !== undefined && window . Math === Math
? window
: globalThis ;
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
$ . fn . search = function ( parameters ) {
var
$allModules = $ ( this ) ,
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
time = Date . now ( ) ,
performance = [ ] ,
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
query = arguments [ 0 ] ,
methodInvoked = typeof query === 'string' ,
queryArguments = [ ] . slice . call ( arguments , 1 ) ,
returnedValue
;
$allModules . each ( function ( ) {
2022-01-13 13:00:31 +00:00
var
2023-08-17 09:47:40 +00:00
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
2022-01-13 13:00:31 +00:00
;
2023-08-17 09:47:40 +00:00
module = {
initialize : function ( ) {
module . verbose ( 'Initializing module' ) ;
module . get . settings ( ) ;
module . determine . searchFields ( ) ;
module . bind . events ( ) ;
module . set . type ( ) ;
module . create . results ( ) ;
module . instantiate ( ) ;
2022-01-13 13:00:31 +00:00
} ,
2023-08-17 09:47:40 +00:00
instantiate : function ( ) {
module . verbose ( 'Storing instance of module' , module ) ;
instance = module ;
$module
. data ( moduleNamespace , module )
;
2022-01-13 13:00:31 +00:00
} ,
2023-08-17 09:47:40 +00:00
destroy : function ( ) {
module . verbose ( 'Destroying instance' ) ;
$module
. off ( eventNamespace )
. removeData ( moduleNamespace )
;
2022-01-13 13:00:31 +00:00
} ,
2023-08-17 09:47:40 +00:00
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 ) ;
2022-01-13 13:00:31 +00:00
} ,
2023-08-17 09:47:40 +00:00
refreshResults : function ( ) {
$results = $module . find ( selector . results ) ;
$result = $module . find ( selector . result ) ;
} ,
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
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' , module . is . chrome ( ) ? 'fomantic-search' : '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 )
;
} ,
} ,
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
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 ;
}
} ,
} ,
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
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 ( ) ;
2023-12-19 14:36:33 +00:00
module . timer = setTimeout ( function ( ) { module . hideResults ( ) ; } , settings . hideDelay ) ;
2023-08-17 09:47:40 +00:00
}
;
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 . trigger ( '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 )
;
var oldValue = module . get . 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 ( value && module . get . value ( ) === oldValue ) {
module . set . value ( value ) ;
}
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 ( $el ) {
var
elTop ,
elBottom ,
resultsScrollTop ,
resultsHeight
;
if ( $el . length === 0 ) {
return ;
}
elTop = $el . position ( ) . top ;
elBottom = elTop + $el . outerHeight ( true ) ;
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
resultsScrollTop = $results . scrollTop ( ) ;
resultsHeight = $results . height ( ) ;
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
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 ) {
if ( ! module . is . visible ( ) ) {
module . verbose ( 'Escape key pressed, blurring search field' ) ;
$prompt . trigger ( 'blur' ) ;
} else {
module . hideResults ( ) ;
}
event . stopPropagation ( ) ;
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 ) ;
}
}
} ,
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
setup : {
api : function ( searchTerm , callback ) {
var
apiSettings = {
debug : settings . debug ,
on : false ,
cache : settings . cache ,
action : 'search' ,
urlData : {
query : searchTerm ,
} ,
} ,
apiCallbacks = {
onSuccess : function ( response , $module , xhr ) {
module . parse . response . call ( element , response , searchTerm ) ;
callback ( ) ;
if ( settings . apiSettings && typeof settings . apiSettings . onSuccess === 'function' ) {
settings . apiSettings . onSuccess . call ( this , response , $module , xhr ) ;
}
} ,
onFailure : function ( response , $module , xhr ) {
module . displayMessage ( error . serverError ) ;
callback ( ) ;
if ( settings . apiSettings && typeof settings . apiSettings . onFailure === 'function' ) {
settings . apiSettings . onFailure . call ( this , response , $module , xhr ) ;
}
} ,
onAbort : function ( status , $module , xhr ) {
if ( settings . apiSettings && typeof settings . apiSettings . onAbort === 'function' ) {
settings . apiSettings . onAbort . call ( this , status , $module , xhr ) ;
}
} ,
onError : function ( errorMessage , $module , xhr ) {
module . error ( ) ;
if ( settings . apiSettings && typeof settings . apiSettings . onError === 'function' ) {
settings . apiSettings . onError . call ( this , errorMessage , $module , xhr ) ;
}
} ,
}
;
$ . extend ( true , apiSettings , settings . apiSettings , apiCallbacks ) ;
module . verbose ( 'Setting up API request' , apiSettings ) ;
$module . api ( apiSettings ) ;
} ,
} ,
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
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 ;
} ,
} ,
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
is : {
animating : function ( ) {
return $results . hasClass ( className . animating ) ;
} ,
chrome : function ( ) {
return ! ! window . chrome && ! window . StyleMedia ;
} ,
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 ;
} ,
} ,
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
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 ( ) {
return $module . data ( metadata . 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 ;
} ,
} ,
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
select : {
firstResult : function ( ) {
module . verbose ( 'Selecting first result' ) ;
$result . first ( ) . addClass ( className . active ) ;
} ,
} ,
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
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 ( className [ type ] ) {
$module . addClass ( className [ type ] ) ;
}
} ,
buttonPressed : function ( ) {
$searchButton . addClass ( className . pressed ) ;
} ,
} ,
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
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 ;
} ,
} ,
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
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 ) ;
2023-12-19 14:36:33 +00:00
settings . onResults . call ( element , cache . results , true ) ;
2023-08-17 09:47:40 +00:00
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 ( ) ;
}
} ,
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
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
var lastSearchFieldIndex = searchFields . length - 1 ;
$ . each ( source , function ( label , content ) {
var concatenatedContent = [ ] ;
$ . each ( searchFields , function ( index , field ) {
var
fieldExists = ( typeof content [ field ] === 'string' ) || ( typeof content [ field ] === 'number' )
;
if ( fieldExists ) {
var text ;
text = typeof content [ field ] === 'string'
? module . remove . diacritics ( content [ field ] )
: content [ field ] . toString ( ) ;
if ( settings . fullTextSearch === 'all' ) {
concatenatedContent . push ( text ) ;
if ( index < lastSearchFieldIndex ) {
return true ;
}
text = concatenatedContent . join ( ' ' ) ;
}
if ( settings . fullTextSearch !== 'all' && text . search ( matchRegExp ) !== - 1 ) {
// content starts with value (first in results)
addResult ( results , content ) ;
} else if ( settings . fullTextSearch === 'exact' && module . exactSearch ( searchTerm , text ) ) {
addResult ( exactResults , content ) ;
} else if ( settings . fullTextSearch === 'some' && module . wordSearch ( searchTerm , text ) ) {
addResult ( exactResults , content ) ;
} else if ( settings . fullTextSearch === 'all' && module . wordSearch ( searchTerm , text , true ) ) {
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 ( ) ;
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
return term . indexOf ( query ) > - 1 ;
} ,
wordSearch : function ( query , term , matchAll ) {
var allWords = query . split ( /\s+/ ) ,
w ,
wL = allWords . length ,
found = false
;
for ( w = 0 ; w < wL ; w ++ ) {
found = module . exactSearch ( allWords [ w ] , term ) ;
if ( ( ! found && matchAll ) || ( found && ! matchAll ) ) {
break ;
}
}
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
return found ;
} ,
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 ;
}
for ( var characterIndex = 0 , nextCharacterIndex = 0 ; characterIndex < queryLength ; characterIndex ++ ) {
var
continueSearch = false ,
queryCharacter = query . charCodeAt ( characterIndex )
;
while ( nextCharacterIndex < termLength ) {
if ( term . charCodeAt ( nextCharacterIndex ++ ) === queryCharacter ) {
continueSearch = true ;
break ;
}
}
if ( ! continueSearch ) {
return false ;
}
2022-01-13 13:00:31 +00:00
}
2023-08-17 09:47:40 +00:00
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 ,
silent : settings . silent ,
duration : settings . duration ,
onShow : function ( ) {
var $firstResult = $module . find ( selector . result ) . eq ( 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 ,
silent : settings . silent ,
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 = Date . now ( ) ;
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 ) ;
2023-12-19 14:36:33 +00:00
module . performance . timer = setTimeout ( function ( ) { module . performance . display ( ) ; } , 500 ) ;
2023-08-17 09:47:40 +00:00
} ,
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 ( $allModules . length > 1 ) {
title += ' (' + $allModules . length + ')' ;
}
if ( 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 = context || element ;
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 {
module . error ( error . method , query ) ;
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 ( ) ;
2022-01-13 13:00:31 +00:00
}
2023-08-17 09:47:40 +00:00
module . invoke ( query ) ;
} else {
if ( instance !== undefined ) {
instance . invoke ( 'destroy' ) ;
2022-01-13 13:00:31 +00:00
}
2023-08-17 09:47:40 +00:00
module . initialize ( ) ;
2022-01-13 13:00:31 +00:00
}
2023-08-17 09:47:40 +00:00
} ) ;
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 ) { } ,
2023-12-19 14:36:33 +00:00
onResults : function ( response , fromCache ) { } ,
2023-08-17 09:47:40 +00:00
onResultsOpen : function ( ) { } ,
onResultsClose : function ( ) { } ,
className : {
animating : 'animating' ,
active : 'active' ,
category : 'category' ,
empty : 'empty' ,
focus : 'focus' ,
hidden : 'hidden' ,
loading : 'loading' ,
results : 'results' ,
pressed : 'down' ,
2022-01-13 13:00:31 +00:00
} ,
2023-08-17 09:47:40 +00:00
error : {
source : 'Cannot search. No source used, and Fomantic API module was not included' ,
noResultsHeader : 'No Results' ,
noResults : 'Your search returned no results' ,
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.' ,
2022-01-13 13:00:31 +00:00
} ,
2023-08-17 09:47:40 +00:00
metadata : {
cache : 'cache' ,
results : 'results' ,
result : 'result' ,
2022-01-13 13:00:31 +00:00
} ,
2023-08-17 09:47:40 +00:00
regExp : {
escape : /[$()*+./?[\\\]^{|}-]/g ,
beginsWith : '(?:\\s|^)' ,
2022-01-13 13:00:31 +00:00
} ,
2023-08-17 09:47:40 +00:00
// 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
2022-01-13 13:00:31 +00:00
} ,
2023-08-17 09:47:40 +00:00
selector : {
prompt : '.prompt' ,
searchButton : '.search.button' ,
results : '.results' ,
message : '.results > .message' ,
category : '.category' ,
result : '.result' ,
title : '.title, .name' ,
2022-01-13 13:00:31 +00:00
} ,
2023-08-17 09:47:40 +00:00
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 ( /&(?![\d#a-z]{1,12};)/gi , '&' ) ;
return string . replace ( badChars , escapedChar ) ;
2022-01-13 13:00:31 +00:00
}
2023-08-17 09:47:40 +00:00
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>' ;
}
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
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 ) {
html += result [ fields . url ]
? '<a class="result" href="' + result [ fields . url ] . replace ( /"/g , '' ) + '">'
: '<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 ] ) {
html += fields . actionURL === false
? ''
+ '<div class="action">'
+ escape ( response [ fields . action ] [ fields . actionText ] , preserveHTML )
+ '</div>'
: ''
+ '<a href="' + response [ fields . action ] [ fields . actionURL ] . replace ( /"/g , '' ) + '" class="action">'
+ escape ( response [ fields . action ] [ fields . actionText ] , preserveHTML )
+ '</a>' ;
}
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
return html ;
}
return false ;
} ,
standard : function ( response , fields , preserveHTML ) {
var
html = '' ,
escape = $ . fn . search . settings . templates . escape
2022-01-13 13:00:31 +00:00
;
2023-08-17 09:47:40 +00:00
if ( response [ fields . results ] !== undefined ) {
// each result
$ . each ( response [ fields . results ] , function ( index , result ) {
html += result [ fields . url ]
? '<a class="result" href="' + result [ fields . url ] . replace ( /"/g , '' ) + '">'
: '<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 ] ) {
html += fields . actionURL === false
? ''
+ '<div class="action">'
+ escape ( response [ fields . action ] [ fields . actionText ] , preserveHTML )
+ '</div>'
: ''
+ '<a href="' + response [ fields . action ] [ fields . actionURL ] . replace ( /"/g , '' ) + '" class="action">'
+ escape ( response [ fields . action ] [ fields . actionText ] , preserveHTML )
+ '</a>' ;
}
2022-01-13 13:00:31 +00:00
2023-08-17 09:47:40 +00:00
return html ;
}
2022-09-15 12:58:37 +00:00
2023-08-17 09:47:40 +00:00
return false ;
} ,
} ,
} ;
$ . extend ( $ . easing , {
easeOutExpo : function ( x ) {
return x === 1 ? 1 : 1 - Math . pow ( 2 , - 10 * x ) ;
} ,
} ) ;
} ) ( jQuery , window , document ) ;