Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2892348
TypeaheadSource.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
10 KB
Referenced Files
None
Subscribers
None
TypeaheadSource.js
View Options
/**
* @requires javelin-install
* javelin-util
* javelin-dom
* javelin-typeahead-normalizer
* @provides javelin-typeahead-source
* @javelin
*/
/**
* @group control
*/
JX
.
install
(
'TypeaheadSource'
,
{
construct
:
function
()
{
this
.
_raw
=
{};
this
.
_lookup
=
{};
this
.
setNormalizer
(
JX
.
TypeaheadNormalizer
.
normalize
);
this
.
_excludeIDs
=
{};
},
events
:
[
'waiting'
,
'resultsready'
,
'complete'
],
properties
:
{
/**
* Allows you to specify a function which will be used to normalize strings.
* Strings are normalized before being tokenized, and before being sent to
* the server. The purpose of normalization is to strip out irrelevant data,
* like uppercase/lowercase, extra spaces, or punctuation. By default,
* the @{JX.TypeaheadNormalizer} is used to normalize strings, but you may
* want to provide a different normalizer, particiularly if there are
* special characters with semantic meaning in your object names.
*
* @param function
*/
normalizer
:
null
,
/**
* If a typeahead query should be processed before being normalized and
* tokenized, specify a queryExtractor.
*
* @param function
*/
queryExtractor
:
null
,
/**
* Transformers convert data from a wire format to a runtime format. The
* transformation mechanism allows you to choose an efficient wire format
* and then expand it on the client side, rather than duplicating data
* over the wire. The transformation is applied to objects passed to
* addResult(). It should accept whatever sort of object you ship over the
* wire, and produce a dictionary with these keys:
*
* - **id**: a unique id for each object.
* - **name**: the string used for matching against user input.
* - **uri**: the URI corresponding with the object (must be present
* but need not be meaningful)
*
* You can also give:
* - **display**: the text or nodes to show in the DOM. Usually just the
* same as ##name##.
* - **tokenizable**: if you want to tokenize something other than the
* ##name##, for the typeahead to complete on, specify it here. A
* selected entry from the typeahead will still insert the ##name##
* into the input, but the ##tokenizable## field lets you complete on
* non-name things.
*
* The default transformer expects a three element list with elements
* [name, uri, id]. It assigns the first element to both ##name## and
* ##display##.
*
* @param function
*/
transformer
:
null
,
/**
* Configures the maximum number of suggestions shown in the typeahead
* dropdown.
*
* @param int
*/
maximumResultCount
:
5
,
/**
* Optional function which is used to sort results. Inputs are the input
* string, the list of matches, and a default comparator. The function
* should sort the list for display. This is the minimum useful
* implementation:
*
* function(value, list, comparator) {
* list.sort(comparator);
* }
*
* Alternatively, you may pursue more creative implementations.
*
* The `value` is a raw string; you can bind the datasource into the
* function and use normalize() or tokenize() to parse it.
*
* The `list` is a list of objects returned from the transformer function,
* see the `transformer` property. These are the objects in the list which
* match the value.
*
* The `comparator` is a sort callback which implements sensible default
* sorting rules (e.g., alphabetic order), which you can use as a fallback
* if you just want to tweak the results (e.g., put some items at the top).
*
* The function is called after the user types some text, immediately before
* the possible completion results are displayed to the user.
*
* @param function
*/
sortHandler
:
null
},
members
:
{
_raw
:
null
,
_lookup
:
null
,
_excludeIDs
:
null
,
_changeListener
:
null
,
_startListener
:
null
,
bindToTypeahead
:
function
(
typeahead
)
{
this
.
_changeListener
=
typeahead
.
listen
(
'change'
,
JX
.
bind
(
this
,
this
.
didChange
)
);
this
.
_startListener
=
typeahead
.
listen
(
'start'
,
JX
.
bind
(
this
,
this
.
didStart
)
);
},
unbindFromTypeahead
:
function
()
{
this
.
_changeListener
.
remove
();
this
.
_startListener
.
remove
();
},
didChange
:
function
(
value
)
{
return
;
},
didStart
:
function
()
{
return
;
},
clearCache
:
function
()
{
this
.
_raw
=
{};
this
.
_lookup
=
{};
},
addExcludeID
:
function
(
id
)
{
if
(
id
)
{
this
.
_excludeIDs
[
id
]
=
true
;
}
},
removeExcludeID
:
function
(
id
)
{
if
(
id
)
{
delete
this
.
_excludeIDs
[
id
];
}
},
addResult
:
function
(
obj
)
{
obj
=
(
this
.
getTransformer
()
||
this
.
_defaultTransformer
)(
obj
);
if
(
obj
.
id
in
this
.
_raw
)
{
// We're already aware of this result. This will happen if someone
// searches for "zeb" and then for "zebra" with a
// TypeaheadRequestSource, for example, or the datasource just doesn't
// dedupe things properly. Whatever the case, just ignore it.
return
;
}
if
(
__DEV__
)
{
for
(
var
k
in
{
name
:
1
,
id
:
1
,
display
:
1
,
uri
:
1
})
{
if
(
!
(
k
in
obj
))
{
throw
new
Error
(
"JX.TypeaheadSource.addResult(): "
+
"result must have properties 'name', 'id', 'uri' and 'display'."
);
}
}
}
this
.
_raw
[
obj
.
id
]
=
obj
;
var
t
=
this
.
tokenize
(
obj
.
tokenizable
||
obj
.
name
);
for
(
var
jj
=
0
;
jj
<
t
.
length
;
++
jj
)
{
if
(
!
this
.
_lookup
.
hasOwnProperty
(
t
[
jj
]))
{
this
.
_lookup
[
t
[
jj
]]
=
[];
}
this
.
_lookup
[
t
[
jj
]].
push
(
obj
.
id
);
}
},
waitForResults
:
function
()
{
this
.
invoke
(
'waiting'
);
return
this
;
},
/**
* Get the raw state of a result by its ID. A number of other events and
* mechanisms give a list of result IDs and limited additional data; if you
* need to act on the full result data you can look it up here.
*
* @param scalar Result ID.
* @return dict Corresponding raw result.
*/
getResult
:
function
(
id
)
{
return
this
.
_raw
[
id
];
},
matchResults
:
function
(
value
,
partial
)
{
// This table keeps track of the number of tokens each potential match
// has actually matched. When we're done, the real matches are those
// which have matched every token (so the value is equal to the token
// list length).
var
match_count
=
{};
// This keeps track of distinct matches. If the user searches for
// something like "Chris C" against "Chris Cox", the "C" will match
// both fragments. We need to make sure we only count distinct matches.
var
match_fragments
=
{};
var
matched
=
{};
var
seen
=
{};
var
query_extractor
=
this
.
getQueryExtractor
();
if
(
query_extractor
)
{
value
=
query_extractor
(
value
);
}
var
t
=
this
.
tokenize
(
value
);
// Sort tokens by longest-first. We match each name fragment with at
// most one token.
t
.
sort
(
function
(
u
,
v
)
{
return
v
.
length
-
u
.
length
;
});
for
(
var
ii
=
0
;
ii
<
t
.
length
;
++
ii
)
{
// Do something reasonable if the user types the same token twice; this
// is sort of stupid so maybe kill it?
if
(
t
[
ii
]
in
seen
)
{
t
.
splice
(
ii
--
,
1
);
continue
;
}
seen
[
t
[
ii
]]
=
true
;
var
fragment
=
t
[
ii
];
for
(
var
name_fragment
in
this
.
_lookup
)
{
if
(
name_fragment
.
substr
(
0
,
fragment
.
length
)
===
fragment
)
{
if
(
!
(
name_fragment
in
matched
))
{
matched
[
name_fragment
]
=
true
;
}
else
{
continue
;
}
var
l
=
this
.
_lookup
[
name_fragment
];
for
(
var
jj
=
0
;
jj
<
l
.
length
;
++
jj
)
{
var
match_id
=
l
[
jj
];
if
(
!
match_fragments
[
match_id
])
{
match_fragments
[
match_id
]
=
{};
}
if
(
!
(
fragment
in
match_fragments
[
match_id
]))
{
match_fragments
[
match_id
][
fragment
]
=
true
;
match_count
[
match_id
]
=
(
match_count
[
match_id
]
||
0
)
+
1
;
}
}
}
}
}
var
hits
=
[];
for
(
var
k
in
match_count
)
{
if
(
match_count
[
k
]
==
t
.
length
&&
!
this
.
_excludeIDs
[
k
])
{
hits
.
push
(
k
);
}
}
this
.
sortHits
(
value
,
hits
);
var
nodes
=
this
.
renderNodes
(
value
,
hits
);
this
.
invoke
(
'resultsready'
,
nodes
);
if
(
!
partial
)
{
this
.
invoke
(
'complete'
);
}
},
sortHits
:
function
(
value
,
hits
)
{
var
objs
=
[];
var
ii
;
for
(
ii
=
0
;
ii
<
hits
.
length
;
ii
++
)
{
objs
.
push
(
this
.
_raw
[
hits
[
ii
]]);
}
var
default_comparator
=
function
(
u
,
v
)
{
var
key_u
=
u
.
sort
||
u
.
name
;
var
key_v
=
v
.
sort
||
v
.
name
;
return
key_u
.
localeCompare
(
key_v
);
};
var
handler
=
this
.
getSortHandler
()
||
function
(
value
,
list
,
cmp
)
{
list
.
sort
(
cmp
);
};
handler
(
value
,
objs
,
default_comparator
);
hits
.
splice
(
0
,
hits
.
length
);
for
(
ii
=
0
;
ii
<
objs
.
length
;
ii
++
)
{
hits
.
push
(
objs
[
ii
].
id
);
}
},
renderNodes
:
function
(
value
,
hits
)
{
var
n
=
Math
.
min
(
this
.
getMaximumResultCount
(),
hits
.
length
);
var
nodes
=
[];
for
(
var
kk
=
0
;
kk
<
n
;
kk
++
)
{
nodes
.
push
(
this
.
createNode
(
this
.
_raw
[
hits
[
kk
]]));
}
return
nodes
;
},
createNode
:
function
(
data
)
{
return
JX
.
$N
(
'a'
,
{
sigil
:
'typeahead-result'
,
href
:
data
.
uri
,
name
:
data
.
name
,
rel
:
data
.
id
,
className
:
'jx-result'
},
data
.
display
);
},
normalize
:
function
(
str
)
{
return
this
.
getNormalizer
()(
str
);
},
tokenize
:
function
(
str
)
{
str
=
this
.
normalize
(
str
);
if
(
!
str
.
length
)
{
return
[];
}
return
str
.
split
(
/\s/g
);
},
_defaultTransformer
:
function
(
object
)
{
return
{
name
:
object
[
0
],
display
:
object
[
0
],
uri
:
object
[
1
],
id
:
object
[
2
]
};
}
}
});
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sun, Jan 19, 16:40 (2 w, 5 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1126567
Default Alt Text
TypeaheadSource.js (10 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment