Is there an API for the zing global search system?
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
10-02-2015 09:09 AM
I am looking at this article:
How to Create a Custom Global Search Page - ServiceNow Guru
and I am having a hard time understanding where the actual search occurs. I have an ask to create an interactive search module on our CMS homepage, that is simiar to how google search works with the in page results drop down, and auto complete.
I want to be able to search the knowledge base and catalog items in a script include, and return the resulting urls as an ajax response as the user types in the search input.
Any help with identifying which code lines in the article above can help get me started on building a simple script include that takes a set of text parameters, and runs a glide query to return sys_ids for matching pages in those groups would be appreciated.
Is there any concise documentation around that explains how to interface with the search system from a jelly script.
thanks
- Labels:
-
Scripting and Coding
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
10-03-2015 06:25 AM
Hm... I think, in your case, it may be much easier to query the actual tables using GlideRecord in Script Includes and then format the result accordingly.
Using GlideRecord to Query Tables - ServiceNow Wiki
The table for Knowledge Base Article should be kb_knowledge and the table for Catalog Items should be sc_req_item.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
10-05-2015 05:34 AM
Deryck,
After researching most of the day Friday and attempting to leverage Zing, that is the conclusion I reached at well. Disappointing.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
10-14-2015 11:22 AM
My Search Modules (if anyone is interested)
End result:
Script Include - Ajax Search
var CMS_Ajax_Search = Class.create();
CMS_Ajax_Search.prototype = Object.extendsObject(AbstractAjaxProcessor, {
/**
glide ajax server side script - provides methods to populate the CMS v2 real-time search module via ajax client server interaction
@class CMS_Ajax_Search
*/
/**
save the search string parameter to the local options object
@method _saveSearchStr
@param {string} str
*/
_saveSearchStr: function(str){
this._options = {
'search_terms' : ((this._isString(str) && str.length )? str : ''),
};
this._saveSearchTermTrackingRecord();
},
/**
return results from the ts_query table that have phases matching the parameter ("starts with")
@method findAutoCompleteMatches
@param {string} sysparm_data (ajax) contained in the class parameter object under key 'phrase' - phrase used to search for matching user query terms
@param {string} str function parameter used for testing in stead of the ajax parameter
*/
findAutoCompleteMatches : function (str){
var json = new JSON();
var autocomplete_str = '';
var params = json.decode(this.getParameter('sysparm_data'));
var results_lc = [];
var result = {
'error' : false,
'data' : [],
'type' : '',
};
if (params || str){
try {
if (this._isObject(params) && this._has(params, 'phrase') && this._isString(params.phrase)){
autocomplete_str = params.phrase;
// check for GUID from client transaction, save to response if it exists
if (this._has(params, 'message_id') && this._isString(params.message_id)){
result.message_id = params.message_id;
}
// check for "type" parameter, and save to response - tells response parser which handler to use
if (this._has(params, 'type') && this._isString(params.type)){
result.type = params.type;
}
}
else if (this._isString(str)){
autocomplete_str = str;
}
else {
throw new Error("Error: the required parameter property \"phrase\" was not found");
}
// query for results matching phrase parameter and save to result object
var term = '', term_lc ='';
var gr = new GlideRecord('ts_query');
gr.addQuery('sys_created_on', '>=',gs.monthsAgoStart(3));
gr.addQuery('search_term','STARTSWITH', autocomplete_str);
gr.addQuery('sys_mod_count','>', 0);
gr.orderByDesc('sys_mod_count');
gr.query();
while (gr.next()) {
term = gr.search_term.getDisplayValue();
term_lc = term.toLowerCase();
if (this._inArray(term_lc, results_lc) < 0){
results_lc.push(term_lc)
result.data.push(term);
}
}
}
catch(e){
result.error = true;
result.message = "Error: "+e.message+" - findAutoCompleteMatches::"+this.type;
}
}
else{
result.error = true;
result.message = 'parameter "search string" is invalid - findAutoCompleteMatches::'+this.type;
}
return json.encode(result);
},
/**
return json encoded results from the catalog items, knowledge articles and requested item search
@method getSeachResults
@param {string} sysparm_data (ajax) contained in the class parameter object under key 'search_string' - phrase used to search for matching catalog items, knowledge articles and requested items
@param {string} str function parameter used for testing in stead of the ajax parameter
@returns string
*/
getSeachResults : function(str){
var json = new JSON();
var params = json.decode(this.getParameter('sysparm_data'));
var result = {
'error' : false,
'data' : [],
'type' : '',
};
if (params || str){
try {
if (this._isObject(params) && this._has(params, 'search_string') && this._isString(params.search_string)){
this._saveSearchStr(params.search_string);
// check for GUID from client transaction, save to response if it exists
if (this._has(params, 'message_id') && this._isString(params.message_id)){
result.message_id = params.message_id;
}
// check for "type" parameter, and save to response - tells response parser which handler to use
if (this._has(params, 'type') && this._isString(params.type)){
result.type = params.type;
}
// save the search term to the response so it can be used for highlighting in the results presentation
result.search_term = params.search_string;
}
else if (this._isString(str)){
this._saveSearchStr(str);
}
else {
throw new Error("Error: the required parameter property \"search_string\" was not found");
}
// query for results matching search parameter and save to result object
result.data.push({
'category': 'requested_items',
'data' : this._queryRequests()
});
result.data.push({
'category': 'catalog_items',
'data' : this._queryCatalog()
});
result.data.push({
'category': 'knowledge_articles',
'data' : this._queryKnowledge()
});
}
catch(e){
result.error = true;
result.message = "Error: "+e.message+" - get::"+this.type;
}
}
else{
result.error = true;
result.message = 'parameter "search string" is invalid - get::'+this.type;
}
return json.encode(result);
},
/**
options used to perform request, catalog item, and knowledge article searching
@property _options
*/
_options : {},
/**
save the search term the user submitted to the ts_query table for use by Zing search
@method _saveSearchTermTrackingRecord
*/
_saveSearchTermTrackingRecord : function(){
if (this._isString(this._options.search_terms) && (this._trim(this._options.search_terms)).length){
var gr = new GlideRecord('ts_query');
gr.addQuery('user',gs.getUserID());
gr.addQuery('search_term',this._options.search_terms);
gr.query();
while (gr.next()) {
gr.recent = "false";
gr.update();
}
gr.initialize();
gr.user.setValue(gs.getUserID());
gr.search_term = this._options.search_terms;
gr.recent = "true";
gr.insert();
}
},
/**
query the knowledge articles table based on the string parameter saved when class initialized
save results to this._data['knowledge_articles']
@method _queryKnowledge
@returns {array}
*/
_queryKnowledge : function(){
gs.log(this._isString(this._options.search_terms));
gs.log(this._trim(this._options.search_terms).length);
if (this._isString(this._options.search_terms) && (this._trim(this._options.search_terms).length)){
var data = [];
var know_item = new GlideRecord('kb_knowledge');
know_item.addQuery('workflow_state','published');
know_item.addQuery('topic', '!=', 'Alerts');
know_item.addQuery('123TEXTQUERY321', this._options.search_terms);
know_item.orderByDesc('sys_view_count');
know_item.orderByDesc('published');
know_item.setLimit(10);
know_item.query();
while (know_item.next()) {
var obj = {
'category' : know_item.kb_category.title.getDisplayValue(),
'sys_id' : know_item.sys_id.getDisplayValue(),
'title': know_item.name.getDisplayValue(),
'intro' : know_item.description.getDisplayValue(),
'descr' : know_item.short_description.getDisplayValue()
};
data.push(obj);
}
}
return data;
},
/**
query the catalog item table based on the string parameter saved when class initialized
save results to this._data['catalog_items']
@method _queryCatalog
@returns {array}
*/
_queryCatalog : function(){
var data = {};
if (this._isString(this._options.search_terms) && (this._trim(this._options.search_terms).length)){
var cat_item = new GlideRecord('sc_cat_item');
var category = '';
//type!=bundle^sys_class_name!=sc_cat_item_guide^type!=package^sys_class_name!=sc_cat_item_content^active=true
var categories = {};
cat_item.addQuery('active',true);
cat_item.addQuery('type', '!=', 'bundle');
// include catalog guides
/* cat_item.addQuery('sys_class_name', '!=', 'sc_cat_item_guide'); */
cat_item.addQuery('type', '!=', 'package');
cat_item.addQuery('123TEXTQUERY321', this._options.search_terms);
// do not sort, results should be returned by relevance as default behavior
//cat_item.orderByDesc('name');
cat_item.setLimit(10);
cat_item.query();
while (cat_item.next()) {
// assemble categories
category = cat_item.category.title.getDisplayValue() || cat_item.u_business_service.getDisplayValue() || 'Uncategorized';
if (!categories.hasOwnProperty(category)){
data[category] = [];
}
}
// reset
cat_item.restoreLocation(-1);
while (cat_item.next()) {
category = cat_item.category.title.getDisplayValue() || cat_item.u_business_service.getDisplayValue() || 'Uncategorized';
var is_order_guide = false;
// determine if the item is an order guide
var pos = (cat_item.sys_class_name.getDisplayValue()).toLowerCase().indexOf('guide');
if (pos >= 0){
is_order_guide = true;
}
var obj = {
'sys_id' : cat_item.sys_id.getDisplayValue(),
'title': cat_item.name.getDisplayValue(),
'descr' : cat_item.short_description.getDisplayValue(),
'guide' : is_order_guide,
};
data[category].push(obj);
}
}
return data;
},
/**
query the requested item table based on the string parameter saved when class initialized
save results to this._data['requested_items']
@method _queryRequests
*/
_queryRequests : function(){
var data = [];
if (this._isString(this._options.search_terms) && (this._trim(this._options.search_terms).length)){
var req_item = new GlideRecord('sc_req_item');
req_item.addQuery('opened_by', gs.getUserID());
req_item.addQuery('sys_created_on', '>=', gs.daysAgoStart(60));
req_item.addQuery('123TEXTQUERY321', this._options.search_terms);
req_item.orderByDesc('opened_at');
req_item.setLimit(5);
req_item.query();
while (req_item.next()) {
var obj = {
'req_num': req_item.request.number.getDisplayValue(),
'title': req_item.cat_item.name.getDisplayValue(),
'sys_id' : req_item.sys_id.getDisplayValue(),
'state': req_item.state.getDisplayValue(),
'button_class' : this._getButtonClass(req_item.state.getDisplayValue(), req_item.stage.getDisplayValue()),
'sys_created_on': this._formatGlideDate(req_item, 'sys_created_on'),
};
data.push(obj);
}
}
return data;
},
/**
private method to get the requested item bootsrap3 button class from the request state
@method _getButtonClass
@param {string} state workflow state
@param {string} stage workflow stage
*/
_getButtonClass : function(state, stage){
var btn_class = "primary";
var wf_status_lc = (stage + ' ' + state).toLowerCase();
if (wf_status_lc.indexOf('incomplete') >=0 || wf_status_lc.indexOf('cancelled') >=0 || wf_status_lc.indexOf('rejected') >=0){
btn_class = "danger";
}
else if (wf_status_lc.indexOf('complete') >=0){
btn_class = "success";
}
return btn_class;
},
/**
private method to test if an object contains a property
@method _has
@param {object} obj
@param {string} prop object key to verify exist in object
*/
_has: function(obj, prop) {
var result = false;
if (obj && typeof obj === "object" && this._isString(prop) && prop.length){
if (obj.hasOwnProperty(prop)){
result = true;
}
}
return result;
},
/**
determine if parameter is a string
@method _isString
@param {object} str
@link https://github.com/jashkenas/underscore/blob/master/underscore.js
*/
_isString : function(str) {
return Object.prototype.toString.call(str) === '[object String]';
},
/**
determine if parameter is an Object
@method _isObject
@param {object} obj
*/
_isObject : function(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
},
/**
The trim() method removes white space from both ends of a string
@method _trim
@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim
@param {string} str
@returns {string}
*/
_trim : function(str){
// trim polyfill
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
};
}
return str.trim();
},
/**
convert glide record date to MM/DD/YYYY format
@method _formatGlideDate
@param {object} rec glide record
@param {string} column target column to read from glide record
@param {boolean} include_time defaults to false, include the time in the result
@returns integer
*/
_formatGlideDate : function(rec, column, include_time){
var date_str = '';
try{
if (this._has(rec, column)){
var gdt = new GlideDateTime(rec[column]);
date_str = this._zeroPad(gdt.getMonth(),2)+'/'+this._zeroPad(gdt.getDayOfMonthLocalTime(),2)+'/'+gdt.getYear();
if (typeof include_time === 'boolean' && include_time){
var date_str2 = rec[column].getDisplayValue();
date_str += date_str2.substr(date_str2.indexOf(' '));
}
}
}
catch(e){
gs.log(e.message, 'formatGlideDate'+this.type);
}
return date_str;
},
/**
left pad digits with zeros
@method _zeroPad
@link http://stackoverflow.com/questions/2998784/how-to-output-integers-with-leading-zeros-in-javascript
@param {number} number digit to be padded
@param {number} places integer of the amount of zero padding to apply to the digit
@returns {number}
*/
_zeroPad : function (num, places) {
var zero = places - num.toString().length + 1;
return Array(+(zero > 0 && zero)).join("0") + num;
},
/**
@method inArray
@param {object|string|number} needle item to search for in the array
@param {array} haystack array to search
@link http://stackoverflow.com/questions/5864408/javascript-is-in-array
*/
_inArray : function (needle, haystack){
var result = -1;
if (this._isArray(haystack)){
for (var i=0, len=haystack.length; i<len; i++) {
if (haystack[i] === needle){
result = i;
break;
}
}
}
return result;
},
/**
determine if parameter is an array
returns the index of the item, or -1 if not found
@method _isArray
@param {object} arr
@returns {integer}
@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
*/
_isArray : function(arr) {
return Object.prototype.toString.call(arr) === '[object Array]';
},
type: 'CMS_Ajax_Search'
});
Script Include - CMS v2 search
/**
methods to query the DB via glidejax to provide the client a autocomplete hints and search results
UI script
@class CMSv2SearchResults
@filename CMS-v2-search-results
Requires underscore.js and loglevel.js
*/
function CMSv2SearchResults (obj) {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
};
}
/**
track each ajax call with a GUID, and only process results from the most recent ajax call
@property autocomplete_calls
@property search_calls
*/
this.autocomplete_calls = [];
this.search_calls = [];
this.previous_search = '';
// ensure logging package exists, if not provide fallback
if (!window.log){
window.log = {
'error': function(str){ console.log(str); },
'info': function(str){ console.log(str); },
'warn': function(str){ console.log(str); },
'debug': function(str){ console.log(str); },
};
}
/**
detect user deleting characters from short strings with a previous value to compare to so the results can be updated
@property autocomplete_calls
*/
if (typeof window._ !== 'undefined'){
this._init(jQuery);
}
else{
log.error('Error: Underscore.js not detected - CMSv2SearchResults');
}
}
CMSv2SearchResults.prototype = {
constructor : CMSv2SearchResults,
type: 'CMSv2SearchResults',
/**
generate pseudo guids clientside
@method createGUID
@link http://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
*/
createGUID : function(){
var result = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
return result;
},
/**
initialization for the class
@method _init
@param {object} $ jquery
*/
_init : function($){
var context = this;
if($){
$(document).ready(function(){
log.info('Search Results Init');
// define all the info about the various jquery references
// that need to be cached from the DOM
var binding_element_meta = [
{'name': '$wrapper', 'selector': 'body > .wrapper', 'label': 'cms page wrapper'},
{'name': '$srch_results', 'selector': '#large_search_results', 'label': 'search results wrap'},
{'name': '$srch_input', 'selector': '#large-search-tile-search-box input', 'label': 'search input'},
{'name': '$ajax_ldr', 'selector': '#large-search-tile-search-box .ajax-icon-wrap', 'label': 'ajax loader graphic'},
{'name': '$body', 'selector': 'body', 'label': 'HTML BODY'},
{'name': '$srch_btn', 'selector': '#large-search-tile-search-box button', 'label': 'search button'},
{'name': '$hint_wrap', 'selector': '#auto_complete_hints', 'label': 'auto-complete hint'}
];
var binding_results = [];
// perform jQuery DOM element caching
_.each(binding_element_meta, function(obj, key){
//log.info(obj)
binding_results.push(this._savejQueryElemRef($, obj.name, obj.selector, obj.label));
}, context);
// only proceed if all DOM cache ops are successful
if (_.every(binding_results, function(value){ return (_.isBoolean(value) && value); })){
context._bindBodyClickListener($)
context._bindSearchInputKeypressListener($);
context._bindHintClickListener($);
context._bindSearchBtnClickListener($);
context._bindPubSubListener($);
}
else{
log.error('Error: jQuery DOM element caching failed - _init::'+context.type);
}
});
}
else{
log.error('Error: jQuery not detected - _init::'+this.type);
}
},
/**
cache reference to a DOM element for use by the app
@method _savejQueryElemRef
@param {object} jquery prevent jquery / prototype collision
@param {string} var_name name of variable to save the jquery reference to - stored in this[<var_name>]
@param {string} selector DOM selector to feed into jQuery
@param {string} label optional label to be used in warning messages this function will emit if element not found
@return {boolean} true if element was cached
*/
_savejQueryElemRef : function($, var_name, selector, label){
var result = true;
try{
// http://stackoverflow.com/questions/2754818/how-to-detect-what-library-is-behind-the-function
if (!(typeof $ === 'function' && $.fn && $.fn.jquery)){
throw new Error('invalid parameter - jQuery');
}
if(!_.isString(var_name) && var_name.length){
throw new Error('invalid parameter - assignemnt variable name');
}
if(!_.isString(selector) && selector.length){
throw new Error('invalid parameter - DOM selector');
}
if(!_.isString(label) && label.length){
label = var_name;
}
if (!(this[var_name] && this[var_name].length)){
this[var_name] = $(selector);
//log.info(this[var_name])
if (!(this[var_name] && this[var_name].length)){
log.info('Warning: '+label+' target DOM element not found _savejQueryElemRef::'+this.type);
result = false;
}
}
}
catch(e){
log.error('Error: '+e.message+' _savejQueryElemRef::'+this.type);
}
return result;
},
/**
bind the listener for clicks on document body
used to close the search hint and results window
also detects return of focus to the search input, and will redisplay results if the search term has not changed
@method _bindBodyClickListener
@param {object} jquery - send jquery as parameter to ensure no collision with prototype
@listens <body>~click
*/
_bindBodyClickListener : function($){
var context = this;
if (!_.isString(this.$body.attr('data-click-bound'))){
this.$body.attr('data-click-bound', 'false')
}
if (this.$body.attr('data-click-bound') === 'false'){
this.$body.on('click', function(e){
log.info('Body Click');
if(!$(e.target).parents('#large-search-tile-search-box').length) {
// clicked outside the search results and hint window
context._toggleSearchResults(false, false); // non destructive hide
context._toggleHintResults(false);
}
else{
// clicked in search results area
// check for click in the input, and redisplay search results if the string match
if ($(e.target).prop('tagName').toLowerCase() === 'input'){
log.info('REFOCUS in search input');
log.info($(e.target).val())
log.info(context.previous_search)
if ($(e.target).val() === context.previous_search){
log.info('search term has not changed');
context._toggleSearchResults(true);
}
}
}
});
this.$body.attr('data-click-bound', 'true');
}
},
/**
bind the listener for clicks on the search submit button
@method _bindSearchBtnClickListener
@param {object} jquery - send jquery as parameter to ensure no collision with prototype
@listens #large-search-tile-search-box button ~ click
*/
_bindSearchBtnClickListener : function($){
var context = this;
if (this.$srch_btn.attr('data-click-bound') === 'false'){
this.$srch_btn.on('click', function(e){
e.stopPropagation();
log.info('Search Submit Click');
context._getSearchResults(context.$srch_input.val());
});
this.$srch_btn.attr('data-click-bound', 'true');
}
},
/**
bind the listener for clicks on the list of search autocomplete hints
@method _bindHintClickListener
@param {object} jquery - send jquery as parameter to ensure no collision with prototype
@listens #auto_complete_hints li ~ click
*/
_bindHintClickListener : function($){
var context = this;
if (this.$hint_wrap.attr('data-click-bound') === 'false'){
this.$hint_wrap.on('click','li a', function(e){
e.stopPropagation();
context.$srch_input.val($(this).text());
context._toggleHintResults(false);
context._getSearchResults(context.$srch_input.val());
});
this.$hint_wrap.attr('data-click-bound', 'true');
}
},
/**
toggle on and off the ajax loader graphic
@method _toggleAjaxLoader
@param {boolean} enable if true, display loader
*/
_toggleAjaxLoader : function(enable){
show_loader = false;
if (_.isBoolean(enable) && enable){
show_loader = true;
}
if (show_loader){
this.$ajax_ldr.removeClass('hidden');
}
else{
this.$ajax_ldr.addClass('hidden');
}
},
/**
toggle on and off the search results container visibility
destructive method - removes current contents on hide
@method _toggleSearchResults
@param {boolean} enable if true, display content container
@param {boolean} destructive remove current contents of the search drop-down - defaults to true
*/
_toggleSearchResults : function(enable, destructive){
show_content = false;
if (!_.isBoolean(destructive)){
destructive = true;
}
if (_.isBoolean(enable) && enable){
show_content = true;
}
if (show_content){
this.$srch_results.removeClass('hidden');
}
else{
if(destructive){
this.$srch_results.empty();
}
this.$srch_results.addClass('hidden');
}
},
/**
toggle on and off the autocomplete hint results container visibility
destructive method - removes current contents on hide
@method _toggleHintResults
@param {boolean} enable if true, display content container
*/
_toggleHintResults : function(enable){
show_content = false;
if (_.isBoolean(enable) && enable){
show_content = true;
}
if (show_content){
this.$hint_wrap.removeClass('hidden');
}
else{
this.$hint_wrap.find('ul').empty();
this.$hint_wrap.addClass('hidden');
}
},
/**
begin the ajax call to get search results based on user search term
@method _getSearchResults
@param {string} str
*/
_getSearchResults : function(str){
var context = this;
/**
provides a wrapper to the auto-complete callback that's a member of this class
@method callback
*/
var callback = function(response){
context._ajaxCallBack(response);
};
if (_.isString(str)){
this.previous_search = str;
var ajax_id = this.createGUID();
var json_data = JSON.stringify({
'message_id' : ajax_id,
'search_string' : str.trim(),
'type': 'search',
});
this._toggleAjaxLoader(true);
var ga = new GlideAjax('CMS_Ajax_Search');
ga.addParam('sysparm_name','getSeachResults');
ga.addParam('sysparm_data',json_data);
this.search_calls.push(ajax_id);
ga.getXML(callback);
}
},
/**
begin the ajax call to gather search input autocomplete hints
@method _getAutoCompleteHints
@param {string} str
*/
_getAutoCompleteHints : function(str){
var context = this;
/**
provides a wrapper to the auto-complete callback that's a member of this class
@method callback
*/
var callback = function(response){
context._ajaxCallBack(response);
};
if (_.isString(str)){
var ajax_id = this.createGUID();
var json_data = JSON.stringify({
'message_id' : ajax_id,
'phrase' : str,
'type': 'hint',
});
this._toggleAjaxLoader(true);
var ga = new GlideAjax('CMS_Ajax_Search');
ga.addParam('sysparm_name','findAutoCompleteMatches');
ga.addParam('sysparm_data',json_data);
this.autocomplete_calls.push(ajax_id);
ga.getXML(callback);
}
},
/**
bind the listener for input on the search module to provide list of auto-complete hints
@method _bindSearchInputKeypressListener
@param {object} jquery - send jquery as parameter to ensure no collision with prototype
@listens #large-search-tile-search-box input ~ keypress keyup
*/
_bindSearchInputKeypressListener : function($){
var context = this;
if (this.$srch_input.attr('data-keyup-bound') === 'false'){
this.$srch_input.on('keyup keypress' , function(e) {
e.stopPropagation();
//log.info(e.type)
var value = $(this).val();
if ( e.which == 13 ) {
e.preventDefault();
log.info('search submit - enter key');
context._getSearchResults(context.$srch_input.val());
}
else{
if (e.type === 'keyup'){
if(e.which == 8 || e.which == 46){
// backspace pressed
if (value.length){
context._getAutoCompleteHints(value);
}
else{
context._toggleHintResults(false);
}
}
}
else{
if (value.length > 1){
//log.info('Press: '+value)
context._getAutoCompleteHints(value);
}
else{
//log.info('empty')
context._toggleHintResults(false);
}
}
}
});
context.$srch_input.attr('data-keyup-bound', 'true');
}
},
/**
bind event that listens for pub-sub style communication on the body element
@method _bindPubSubListener
@param {object} jquery pass in jquery reference to prevent collisions with prototype.js
*/
_bindPubSubListener : function($){
var context = this;
if (!_.isString(this.$wrapper.attr('data-pubsub-search-bound'))){
this.$wrapper.attr('data-pubsub-search-bound', 'false');
}
if (this.$wrapper.attr('data-pubsub-search-bound') === 'false'){
/**
listen for broadcast pubsub messages from other classes
@method anonymous fx
@listens trigger cms:message
*/
this.$wrapper.on('cms:message', function(e,data){
log.info('Search - pubsub message recieved');
log.info(data);
if (_.has(data, 'search') && data.search === 'close'){
/**
listen for broadcast notice to the search results and hint dropdown menus
@method anonymous fx
@listens cms:message data {search: close}
**/
context._toggleSearchResults(false, false); // non destructive hide
context._toggleHintResults(false);
if (_.has(data, 'clear') && _.isBoolean(data.clear) && data.clear){
context.$srch_input.val('');
}
}
});
this.$wrapper.attr('data-pubsub-search-bound', 'true');
}
},
/**
process server response from ajax submissions
add route to the correct response handler
@method _ajaxCallBack
@param {object} response
*/
_ajaxCallBack : function (response) {
var parsed_obj, answer = response.responseXML.documentElement.getAttribute("answer");
try{
if (_.isString(answer)){
parsed_obj = JSON.parse(answer);
if (parsed_obj && _.isObject(parsed_obj)) {
if (_.has(parsed_obj, 'error') && parsed_obj.error && _.has(parsed_obj, 'message')){
throw new Error(parsed_obj.message);
}
if (!(_.has(parsed_obj, 'type') && _.isString(parsed_obj.type) && parsed_obj.type.length)){
throw new Error('The property "type" is missing or blank. Cannot determine handler function for ajax data');
}
else if (_.has(parsed_obj, 'message_id') && _.isString(parsed_obj.message_id)){
//log.info(parsed_obj.message_id);
var last_element, tracking_arr_name;
switch(parsed_obj.type){
case 'search':
tracking_arr_name = 'search_calls';
break;
case 'hint':
tracking_arr_name = 'autocomplete_calls';
break;
default:
throw new Error('No Matches found for the property "type". Cannot determine handler function for ajax data');
break;
}
last_element = (this[tracking_arr_name].length-1);
if (this[tracking_arr_name][last_element] === parsed_obj.message_id){
// RECIEVED DATA FROM MOST RECENT AJAX REQUEST
this[tracking_arr_name] = [];
this._toggleAjaxLoader(false);
if ( _.has(parsed_obj, 'data')){
this._toggleSearchResults(false);
this._toggleHintResults(false);
switch(parsed_obj.type){
case 'search':
this._searchAjaxResponseHandler(parsed_obj.data, parsed_obj.search_term);
break;
case 'hint':
this._hintAjaxResponseHandler(parsed_obj.data);
break;
}
}
else{
throw new Error("ajax response payload is in unexpected format");
}
}
else{
log.info('stale ajax request detected for transaction "'+parsed_obj.message_id+'" - autocomplete_cb::'+this.type);
}
}
else{
throw new Error("ajax response missing message GUID parameter");
}
}
else{
throw new Error("failed to parse ajax response");
}
}
else{
throw new Error('invalid data type for the ajax response payload');
}
}
catch(e){
log.error('Error: '+e.message+' - _ajaxCallBack::'+this.type);
}
},
/**
process the parsed data from the auto-complete ajax submissions
add hints to the input for the user to choose from
@method _hintAjaxResponseHandler
@param {arr} arr list of hints
*/
_hintAjaxResponseHandler : function(arr){
try{
this.$hint_wrap.find('ul').empty();
if (arr && _.isArray(arr)){
var template_fx = this._getTemplate('srch_tmpl_ac_hint');
_.each(arr, function(value, key){
this.$hint_wrap.find('ul').append(template_fx({'hint_value' : value}));
//log.info(value);
}, this);
if (arr.length){
this._toggleHintResults(true);
}
}
else{
throw new Error('ajax response payload is not of type "array"');
}
}
catch(e){
log.error('Error: '+e.message+' - _hintAjaxResponseHandler::'+this.type);
}
},
/**
process server response from auto-complete ajax submissions
add hints to the input for the user to choose from
@method _searchAjaxResponseHandler
@param {object} obj search results
*/
_searchAjaxResponseHandler : function (obj, search_term) {
try{
this.$srch_results.empty();
if (obj && _.isObject(obj)){
log.info("response: "+JSON.stringify(obj))
_.each(obj, function(obj, key){
if (obj && _.isObject(obj) && _.has(obj, 'category') && _.has(obj, 'data')){
this._searchResultBuilder(obj.category, obj.data, search_term);
}
else{
log.error('Error: ajax response element '+key+' not in expected format - _searchAjaxResponseHandler::'+this.type);
}
}, this);
}
else{
throw new Error("ajax response payload is in unexpected format");
}
}
catch(e){
log.error('Error: '+e.message+' - _searchAjaxResponseHandler::'+this.type);
}
},
/**
expand the search result ajax response data and push to respective templates to be rendered in DOM
@method _searchResultBuilder
@param {array|object} data_obj
@param {string} data_category template identifier
@param {string} search_term search terms user submitted so that result highlighting can occur
*/
_searchResultBuilder : function(data_category, data_obj, search_term){
try{
if (!_.isString(data_category)){
throw new Error('invalid data type for the template idenifier parameter');
}
if (!(_.isArray(data_obj) || (_.isObject(data_obj) && _.keys(data_obj).length))){
throw new Error('invalid data type for data parameter');
}
if (!_.isString(search_term)){
search_term = '';
}
var template_id = 'srch_tmpl_'+data_category;
var template_fx = this._getTemplate(template_id);
if (_.isFunction(template_fx)){
//log.info(data_category+': '+JSON.stringify(data_obj));
this.$srch_results.append(template_fx({'data': data_obj, 'category_name': data_category }));
this._toggleSearchResults(true);
}
else{
throw new Error('template function is undefined for category "'+data_category+'"');
}
}
catch(e){
log.error('Error: '+e.message+' - _searchResultBuilder::'+this.type);
}
},
/**
read underscore.js formatted js template from DOM, and convert to a template function
@method _getTemplate
@param {str} tmpl_id DOM id of the template
@returns {function} underscore template fx
@link http://stackoverflow.com/questions/4778881/how-to-use-underscore-js-as-a-template-engine
*/
_getTemplate : function(tmpl_id){
var result;
var context = this;
// prevent jQuery and prototype collision with an IIFE
(function($){
try{
if (!(_.isString(tmpl_id) && tmpl_id.length)){
throw new Error('Invalid parameter "template id"');
}
// pull up the template and correct for the html encoded brackets
var $templ = $('#'+tmpl_id);
var templ_str = '';
if ($templ && $templ.length){
if ($templ.html().length){
// do a global replace for the html character codes jelly produces for brackets
// second pass required for any brackets encased in html attributes
templ_str = _.unescape($templ.html()).replace(/&lt;/g, '<').replace(/&gt;/g, '>');
//log.info(templ_str);
result = _.template(templ_str);
}
else{
throw new Error('Template "'+tmpl_id+'" is empty. No HTML found to render');
}
}
else{
throw new Error('Template "'+tmpl_id+'" not found in the DOM');
}
}
catch(e){
log.error('Error: '+e.message+' - _getTemplate::'+context.type);
}
})(jQuery);
return result;
},
};
HTML - Search Module (dynamic block)
<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<!-- sys_id: **************************** -->
<g:evaluate>
var user_fname = gs.getUser().getFirstName();
</g:evaluate>
<div id="large-search-tile" class="tile">
<j:set var="jvar_open_tag" value="<%"/>
<j:set var="jvar_close_tag" value=">"/>
<div id="large-search-tile-welcome" class="h2">Hello ${user_fname}, how can we help?</div>
<div class="large-search-wrap">
<div id="large-search-tile-search-box">
<div class="row">
<div class="col-sm-12 padding-left-0 padding-right-0 search-input-wrap">
<input type="text" placeholder="Search" name="large-search-box" data-keyup-bound="false"></input>
<button type="button" name="large-search-box-button" data-click-bound="false"></button>
<div class="ajax-icon-wrap hidden">
<div class="ajax-loader"></div>
</div>
</div>
</div>
<div id="auto_complete_hints" class="row hidden" data-click-bound="false">
<div class="col-sm-12">
<ul class="">
<!-- client side templates rendered here by underscore.js -->
</ul>
</div>
</div>
<div id="large_search_results" class="row hidden" >
<!-- client side templates rendered here by underscore.js -->
</div>
</div>
</div>
</div>
</j:jelly>
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
08-26-2016 09:50 AM
See this:
And:
Document Search API · Issue #97 · service-portal/documentation · GitHub
Server side script to perform a search is in the "Search Page" service portal widget.