mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-08 19:28:44 +02:00
The empty OR condition is now removed completely from the screen and the criterion list. SVN:trunk[5951]
1172 lines
33 KiB
JavaScript
1172 lines
33 KiB
JavaScript
//iTop Search form handler
|
|
;
|
|
$(function()
|
|
{
|
|
// the widget definition, where 'itop' is the namespace,
|
|
// 'search_form_handler' the widget name
|
|
$.widget( 'itop.search_form_handler',
|
|
{
|
|
// default options
|
|
options:
|
|
{
|
|
'criterion_outer_selector': null,
|
|
'result_list_outer_selector': null,
|
|
'data_config_list_selector': null,
|
|
'endpoint': null,
|
|
'init_opened': false,
|
|
'auto_submit': true,
|
|
'search': {
|
|
'base_oql': '',
|
|
'class_name': null,
|
|
'criterion': [
|
|
// Structure
|
|
// {
|
|
// 'or': [
|
|
// {
|
|
// 'and': [
|
|
// {
|
|
// 'ref': 'alias.code',
|
|
// 'operator': 'contains',
|
|
// 'values': [
|
|
// {
|
|
// 'value': 'foo',
|
|
// 'label': 'bar',
|
|
// }
|
|
// ],
|
|
// 'is_removable': true,
|
|
// 'oql': '',
|
|
// },
|
|
// ]
|
|
// },
|
|
// ]
|
|
// },
|
|
],
|
|
'fields': [
|
|
// Structure
|
|
// 'zlist': {
|
|
// 'alias.code': {
|
|
// 'class_alias': '',
|
|
// 'class': '',
|
|
// 'code': '',
|
|
// 'label': '',
|
|
// 'type': '',
|
|
// 'allowed_values': {...},
|
|
// },
|
|
// },
|
|
// 'others': {
|
|
// 'alias.code': {
|
|
// 'class_alias': '',
|
|
// 'class': '',
|
|
// 'code': '',
|
|
// 'label': '',
|
|
// 'type': '',
|
|
// 'allowed_values': {...},
|
|
// },
|
|
// },
|
|
],
|
|
},
|
|
'default_criteria_type': 'raw',
|
|
'conf_parameters': {
|
|
'min_autocomplete_chars': 2,
|
|
'datepicker': {
|
|
'dayNamesMin': ['Su','Mo','Tu','We','Th','Fr','Sa'],
|
|
'monthNamesShort': ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
|
|
'firstDay': 0,
|
|
},
|
|
},
|
|
},
|
|
|
|
// jQuery elements
|
|
elements: null,
|
|
|
|
// Submit properties (XHR, throttle, ...)
|
|
submit: null,
|
|
|
|
// the constructor
|
|
_create: function()
|
|
{
|
|
var me = this;
|
|
|
|
this.element.addClass('search_form_handler');
|
|
|
|
// Init properties (complexe type properties would be static if not initialized with a simple type variable...)
|
|
this.elements = {
|
|
message_area: null,
|
|
criterion_area: null,
|
|
more_criterion: null,
|
|
submit_button: null,
|
|
results_area: null,
|
|
};
|
|
this.submit = {
|
|
xhr: null,
|
|
};
|
|
|
|
//init others widgets :
|
|
this.element.search_form_handler_history({'itop_root_class':me.options.search.class_name});
|
|
|
|
|
|
// Prepare DOM elements
|
|
this._prepareFormArea();
|
|
this._prepareCriterionArea();
|
|
this._prepareResultsArea();
|
|
|
|
// Binding events (eg. from search_form_criteria widgets)
|
|
this._bindEvents();
|
|
|
|
//memorize the initial state so on first criteria close, we do not trigger a refresh if nothing has changed
|
|
this._updateSearch();
|
|
this.oPreviousAjaxParams = JSON.stringify({
|
|
'base_oql': this.options.search.base_oql,
|
|
'criterion': this.options.search.criterion,
|
|
});
|
|
},
|
|
// called when created, and later when changing options
|
|
_refresh: function()
|
|
{
|
|
|
|
},
|
|
// events bound via _bind are removed automatically
|
|
// revert other modifications here
|
|
_destroy: function()
|
|
{
|
|
this.element
|
|
.removeClass('search_form_handler');
|
|
},
|
|
// _setOptions is called with a hash of all options that are changing
|
|
// always refresh when changing options
|
|
_setOptions: function()
|
|
{
|
|
this._superApply(arguments);
|
|
},
|
|
// _setOption is called for each individual option that is changing
|
|
_setOption: function( key, value )
|
|
{
|
|
this._super( key, value );
|
|
},
|
|
|
|
|
|
//
|
|
_bindEvents: function()
|
|
{
|
|
var me = this;
|
|
|
|
// Form events
|
|
// - Prevent regular form submission (eg. hitting "Enter" in inputs)
|
|
this.element.on('submit', function(oEvent){
|
|
oEvent.preventDefault();
|
|
});
|
|
// - Submit the search form
|
|
this.element.on('itop.search.form.submit', function(oEvent, oData){
|
|
me._onSubmit();
|
|
});
|
|
// - Search form has been reloaded by the page
|
|
this.element.on('itop.search.form.reloaded', function(){
|
|
if(me.options.auto_submit === true)
|
|
{
|
|
me._submit();
|
|
}
|
|
});
|
|
|
|
// Criteria events
|
|
this.element.on('itop.search.criteria.value_changed', function(oEvent, oData){
|
|
me._onCriteriaValueChanged(oData);
|
|
});
|
|
this.element.on('itop.search.criteria.removed', function(oEvent, oData){
|
|
me._onCriteriaRemoved(oData);
|
|
});
|
|
this.element.on('itop.search.criteria.error_occured', function(oEvent, oData){
|
|
me._onCriteriaErrorOccured(oData);
|
|
});
|
|
|
|
$('body').on('update_history.itop', function(oEvent, oData) {
|
|
|
|
// if (me.element.parents('.ui-dialog').length != 0)
|
|
// {
|
|
// //search form in modal are forbidden to update history!
|
|
// return;
|
|
// }
|
|
|
|
if ($('.ui-dialog:visible :itop-search_form_handler').length != 0)
|
|
{
|
|
//if a modal containing a search form is visible then the history update event come from it, whe do not want to update the history in this case! because search form in modal are forbidden to update history!
|
|
return;
|
|
}
|
|
|
|
var sNewUrl = GetAbsoluteUrlAppRoot()+'pages/UI.php?operation=search';
|
|
sNewUrl = sNewUrl + '&filter='+oData['filter'];
|
|
sNewUrl = sNewUrl + '&c[menu]='+me._extractURLParameter(window.location.href, "c[menu]");
|
|
sNewUrl = sNewUrl + '&c[org_id]='+me._extractURLParameter(window.location.href, "c[org_id]");
|
|
if ('' != me._extractURLParameter(window.location.href, "debug"))
|
|
{
|
|
sNewUrl = sNewUrl + '&debug='+me._extractURLParameter(window.location.href, "debug");
|
|
}
|
|
|
|
if (typeof history.replaceState != "undefined")
|
|
{
|
|
history.replaceState(null, null, sNewUrl);
|
|
}
|
|
|
|
|
|
$('#itop-breadcrumb')
|
|
.breadcrumb('destroy')
|
|
.breadcrumb({
|
|
itop_instance_id: oData['breadcrumb_instance_id'],
|
|
max_count: oData['breadcrumb_max_count'],
|
|
new_entry: {
|
|
"id": oData['breadcrumb_id'],
|
|
"label": oData['breadcrumb_label'],
|
|
"url": sNewUrl,
|
|
'icon': oData['breadcrumb_icon'],
|
|
'description': ''
|
|
}
|
|
});
|
|
});
|
|
|
|
},
|
|
// - Update search option of the widget
|
|
_updateSearch: function()
|
|
{
|
|
var me = this;
|
|
|
|
// Criterion
|
|
var oCriterion = {
|
|
'or': [{
|
|
'and': []
|
|
}]
|
|
};
|
|
// - Retrieve criterion
|
|
this.elements.criterion_area.find('.sf_criterion_row').each(function(iIdx){
|
|
var oCriterionRowElem = $(this);
|
|
|
|
if (oCriterionRowElem.find('.search_form_criteria').length == 0 && iIdx > 0)
|
|
{
|
|
$(this).remove();
|
|
}
|
|
else
|
|
{
|
|
oCriterionRowElem.find('.search_form_criteria').each(function ()
|
|
{
|
|
var oCriteriaData = $(this).triggerHandler('itop.search.criteria.get_data');
|
|
|
|
if (null != oCriteriaData)
|
|
{
|
|
if (!oCriterion['or'][iIdx])
|
|
{
|
|
oCriterion['or'][iIdx] = {'and': []};
|
|
}
|
|
oCriterion['or'][iIdx]['and'].push(oCriteriaData);
|
|
}
|
|
else
|
|
{
|
|
$(this).remove();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
// - Update search
|
|
this.options.search.criterion = oCriterion;
|
|
|
|
// No need to update base OQL and fields
|
|
},
|
|
// - Open / Close more criterion menu
|
|
_openMoreCriterion: function()
|
|
{
|
|
// Open more criterion menu
|
|
// - Open it first
|
|
this.elements.more_criterion.addClass('opened');
|
|
// - Focus filter
|
|
this.elements.more_criterion.find('.sf_filter:first input[type="text"]')
|
|
.val('')
|
|
.focus();
|
|
// - Then only check if more menu is to close to the right side (otherwise we might not have the right element's position)
|
|
var iFormWidth = this.element.outerWidth();
|
|
var iFormLeftPos = this.element.offset().left;
|
|
var iMenuWidth = this.elements.more_criterion.find('.sfm_content').outerWidth();
|
|
var iMenuLeftPos = this.elements.more_criterion.find('.sfm_content').offset().left;
|
|
if( (iMenuWidth + iMenuLeftPos) > (iFormWidth + iFormLeftPos - 10 /* Security margin */) )
|
|
{
|
|
this.elements.more_criterion.addClass('opened_left');
|
|
}
|
|
},
|
|
_closeMoreCriterion: function()
|
|
{
|
|
this.elements.more_criterion.removeClass('opened_left');
|
|
this.elements.more_criterion.removeClass('opened');
|
|
},
|
|
_toggleMoreCriterion: function()
|
|
{
|
|
// Calling methods instead of toggling the class so additional processing are done.
|
|
if(this.elements.more_criterion.hasClass('opened'))
|
|
{
|
|
this._closeMoreCriterion();
|
|
}
|
|
else
|
|
{
|
|
this._openMoreCriterion();
|
|
}
|
|
},
|
|
// - Close all criterion
|
|
_closeAllCriterion: function()
|
|
{
|
|
this.elements.criterion_area.find('.search_form_criteria.opened').each(function(){
|
|
$(this).triggerHandler('itop.search.criteria.close');
|
|
});
|
|
},
|
|
|
|
// DOM helpers
|
|
// - Prepare form area
|
|
_prepareFormArea: function()
|
|
{
|
|
var me = this;
|
|
|
|
// Build DOM elements
|
|
// - Autosubmit option
|
|
if(this.options.auto_submit === false)
|
|
{
|
|
this.element.addClass('no_auto_submit');
|
|
}
|
|
// - Message area
|
|
this.elements.message_area = this.element.find('.sf_message');
|
|
this._cleanMessageArea();
|
|
|
|
// Events
|
|
// - Refresh icon
|
|
this.element.find('.sft_refresh').on('click', function(oEvent){
|
|
// Prevent anchor
|
|
oEvent.preventDefault();
|
|
// Prevent form toggling
|
|
oEvent.stopPropagation();
|
|
|
|
me._submit();
|
|
});
|
|
// - Toggle icon
|
|
this.element.find('.sf_title').on('click', function(oEvent){
|
|
// Prevent anchors
|
|
oEvent.preventDefault();
|
|
|
|
// Prevent toggle on <select>
|
|
if(oEvent.target.nodeName.toLowerCase() !== 'select' && oEvent.target.nodeName.toLowerCase() !== 'option')
|
|
{
|
|
me.element.toggleClass('closed');
|
|
}
|
|
});
|
|
},
|
|
// - Prepare criterion area
|
|
_prepareCriterionArea: function()
|
|
{
|
|
var oCriterionAreaElem;
|
|
|
|
// Build area element
|
|
if(this.options.criterion_outer_selector !== null && $(this.options.criterion_outer_selector).length > 0)
|
|
{
|
|
oCriterionAreaElem = $(this.options.criterion_outer_selector);
|
|
}
|
|
else
|
|
{
|
|
oCriterionAreaElem = $('<div></div>').appendTo(this.element);
|
|
}
|
|
oCriterionAreaElem.addClass('sf_criterion_area');
|
|
this.elements.criterion_area = oCriterionAreaElem;
|
|
|
|
// Clean area
|
|
oCriterionAreaElem
|
|
.html('')
|
|
.append('<div class="sf_criterion_row"></div>');
|
|
|
|
// Prepare content
|
|
this._prepareMoreCriterionMenu();
|
|
this._prepareExistingCriterion();
|
|
this._prepareSubmitButton();
|
|
},
|
|
// - Prepare "more" button
|
|
_prepareMoreCriterionMenu: function()
|
|
{
|
|
var me = this;
|
|
|
|
// DOM
|
|
this.elements.more_criterion = $('<div></div>')
|
|
.addClass('sf_more_criterion')
|
|
.appendTo(this.elements.criterion_area.find('.sf_criterion_row:first'));
|
|
|
|
// Header part
|
|
var oHeaderElem = $('<div class="sfm_header"></div>')
|
|
.append('<a class="sfm_toggler" href="#"><span class="sfm_tg_title">' + Dict.S('UI:Search:Criterion:MoreMenu:AddCriteria') + '</span><span class="sfm_tg_icon fa fa-plus"></span></a>')
|
|
.appendTo(this.elements.more_criterion);
|
|
|
|
// Content part
|
|
var oContentElem = $('<div class="sfm_content"></div>')
|
|
.appendTo(this.elements.more_criterion);
|
|
|
|
// - Filter
|
|
var oFilterElem = $('<div></div>')
|
|
.addClass('sf_filter')
|
|
.addClass('sfm_filter')
|
|
.append('<span class="sff_input_wrapper"><input type="text" placeholder="' + Dict.S('UI:Search:Value:Filter:Placeholder') + '" /><span class="sff_picto sff_filter fa fa-filter"></span><span class="sff_picto sff_reset fa fa-times"></span></span>')
|
|
.appendTo(oContentElem);
|
|
|
|
// - Lists container
|
|
var oListsElem = $('<div></div>')
|
|
.addClass('sfm_lists')
|
|
.appendTo(oContentElem);
|
|
|
|
// - Recently used list
|
|
var oRecentsElem = $('<div></div>')
|
|
.addClass('sf_list')
|
|
.addClass('sf_list_recents')
|
|
.appendTo(oListsElem);
|
|
|
|
$('<div class="sfl_title"></div>')
|
|
.text(Dict.S('UI:Search:AddCriteria:List:RecentlyUsed:Title'))
|
|
.appendTo(oRecentsElem);
|
|
|
|
var oRecentsItemsElem = $('<ul class="sfl_items"></ul>')
|
|
.append('<li class="sfl_i_placeholder">' + Dict.S('UI:Search:AddCriteria:List:RecentlyUsed:Placeholder') + '</li>')
|
|
.appendTo(oRecentsElem);
|
|
|
|
me._refreshRecentlyUsed();
|
|
|
|
// - Search zlist list
|
|
var oZlistElem = $('<div></div>')
|
|
.addClass('sf_list')
|
|
.addClass('sf_list_zlist')
|
|
.appendTo(oListsElem);
|
|
|
|
$('<div class="sfl_title"></div>')
|
|
.text(Dict.S('UI:Search:AddCriteria:List:MostPopular:Title'))
|
|
.appendTo(oZlistElem);
|
|
|
|
var oZListItemsElem = $('<ul class="sfl_items"></ul>')
|
|
.appendTo(oZlistElem);
|
|
|
|
for(var sFieldRef in this.options.search.fields.zlist)
|
|
{
|
|
var oFieldElem = me._getHtmlLiFromFieldRef(sFieldRef, ['zlist']);
|
|
oFieldElem.appendTo(oZListItemsElem);
|
|
}
|
|
|
|
// - Remaining fields list
|
|
if(this.options.search.fields.others !== undefined)
|
|
{
|
|
var oOthersElem = $('<div></div>')
|
|
.addClass('sf_list')
|
|
.addClass('sf_list_others')
|
|
.appendTo(oListsElem);
|
|
|
|
$('<div class="sfl_title"></div>')
|
|
.text(Dict.S('UI:Search:AddCriteria:List:Others:Title'))
|
|
.appendTo(oOthersElem);
|
|
|
|
var oOthersItemsElem = $('<ul class="sfl_items"></ul>')
|
|
.appendTo(oOthersElem);
|
|
|
|
for(var sFieldRef in this.options.search.fields.others)
|
|
{
|
|
var oFieldElem = me._getHtmlLiFromFieldRef(sFieldRef, ['others']);
|
|
oFieldElem.appendTo(oOthersItemsElem);
|
|
}
|
|
}
|
|
|
|
// - Buttons
|
|
var oButtonsElem = $('<div></div>')
|
|
.addClass('sfm_buttons')
|
|
.append('<button type="button" name="apply">' + Dict.S('UI:Button:Apply') + '</button>')
|
|
.append('<button type="button" name="cancel">' + Dict.S('UI:Button:Cancel') + '</button>')
|
|
.appendTo(oContentElem);
|
|
|
|
// Bind events
|
|
// - Close menu on click anywhere else
|
|
// - Intercept click to avoid propagation (mostly used for closing it when clicking outside of it)
|
|
$('body').on('click', function(oEvent){
|
|
oEventTargetElem = $(oEvent.target);
|
|
|
|
// If not more menu, close all criterion
|
|
if(oEventTargetElem.closest('.sf_more_criterion').length > 0)
|
|
{
|
|
me._closeAllCriterion();
|
|
}
|
|
else
|
|
{
|
|
// TODO: Try to put this back in the date widget as it introduced a non necessary coupling.
|
|
// If using the datetimepicker, do not close anything
|
|
if (oEventTargetElem.closest('#ui-datepicker-div, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-current').length > 0 )
|
|
{
|
|
// No closing in this edge-case introduced by the use of css3's insertion on content using ::before and ::after that pop directly at the body instead of bubbling normally (and passing by their DOM parents)
|
|
}
|
|
// If criteria, close more menu & all criterion but me
|
|
else if(oEventTargetElem.closest('.search_form_criteria').length > 0)
|
|
{
|
|
me._closeMoreCriterion();
|
|
// All criterion but me is already handle by the criterion, no callback needed.
|
|
}
|
|
// If not criteria, close more menu & all criterion
|
|
else
|
|
{
|
|
me._closeMoreCriterion();
|
|
me._closeAllCriterion();
|
|
}
|
|
}
|
|
});
|
|
|
|
// - More criteria toggling
|
|
this.elements.more_criterion.find('.sfm_header').on('click', function(oEvent){
|
|
// Prevent anchor
|
|
oEvent.preventDefault();
|
|
|
|
me._toggleMoreCriterion();
|
|
});
|
|
|
|
// - Filter
|
|
// Note: "keyup" event is use instead of "keydown", otherwise, the inpu value would not be set yet.
|
|
oFilterElem.find('input').on('keyup focus', function(oEvent){
|
|
// TODO: Move on values with up and down arrow keys; select with space or enter.
|
|
// TODO: Hide list if no result on filter.
|
|
|
|
var sFilter = $(this).val();
|
|
|
|
// Show / hide items
|
|
if(sFilter === '')
|
|
{
|
|
oListsElem.find('.sfl_items > li').show();
|
|
oFilterElem.find('.sff_filter').show();
|
|
oFilterElem.find('.sff_reset').hide();
|
|
}
|
|
else
|
|
{
|
|
oListsElem.find('.sfl_items > li:not(.sfl_i_placeholder)').each(function(){
|
|
var oRegExp = new RegExp(sFilter.latinize(), 'ig');
|
|
var sValue = $(this).find('input').val();
|
|
var sLabel = $(this).text();
|
|
|
|
// We don't check the sValue as it contains the class alias.
|
|
if(sLabel.latinise().match(oRegExp) !== null)
|
|
{
|
|
$(this).show();
|
|
}
|
|
else
|
|
{
|
|
$(this).hide();
|
|
}
|
|
});
|
|
oFilterElem.find('.sff_filter').hide();
|
|
oFilterElem.find('.sff_reset').show();
|
|
}
|
|
|
|
// Show / hide lists with no visible items
|
|
oListsElem.find('.sf_list').each(function(){
|
|
$(this).show();
|
|
if($(this).find('.sfl_items > li:visible').length === 0)
|
|
{
|
|
$(this).hide();
|
|
}
|
|
});
|
|
});
|
|
oFilterElem.find('.sff_filter').on('click', function(){
|
|
oFilterElem.find('input').trigger('focus');
|
|
});
|
|
oFilterElem.find('.sff_reset').on('click', function(){
|
|
oFilterElem.find('input')
|
|
.val('')
|
|
.trigger('focus');
|
|
});
|
|
|
|
// - Add one criteria
|
|
this.elements.more_criterion.on('click', '.sfm_field', function(oEvent){
|
|
// Prevent anchor
|
|
oEvent.preventDefault();
|
|
// Prevent propagation to not close the opening criteria
|
|
oEvent.stopPropagation();
|
|
|
|
// If no checkbox checked, add criteria right away, otherwise we "queue" it we other checkboxed.
|
|
if(me.elements.more_criterion.find('.sfm_field input[type="checkbox"]:checked').length === 0)
|
|
{
|
|
var sFieldRef = $(this).attr('data-field-ref');
|
|
|
|
// Prepare new criterion data (as already opened to increase UX)
|
|
var oData = {
|
|
'ref': sFieldRef,
|
|
'init_opened': (oEvent.ctrlKey) ? false : true,
|
|
};
|
|
|
|
// Add criteria but don't submit form as the user has not specified the value yet.
|
|
me.element.search_form_handler_history('setLatest', sFieldRef);
|
|
me._refreshRecentlyUsed();
|
|
me._addCriteria(oData);
|
|
}
|
|
else
|
|
{
|
|
$(this).find('input[type="checkbox"]').prop('checked', !$(this).find('input[type="checkbox"]').prop('checked'));
|
|
}
|
|
});
|
|
|
|
// - Add several criterion
|
|
this.elements.more_criterion.on('click', '.sfm_field input[type="checkbox"]', function(oEvent){
|
|
// Prevent propagation to field and instant add of the criteria
|
|
oEvent.stopPropagation();
|
|
|
|
if(me.elements.more_criterion.find('.sfm_field input[type="checkbox"]:checked').length === 0)
|
|
{
|
|
oButtonsElem.hide();
|
|
}
|
|
else
|
|
{
|
|
oButtonsElem.show();
|
|
}
|
|
|
|
// Put focus back to filter to improve UX.
|
|
oFilterElem.find('input').trigger('focus');
|
|
});
|
|
oButtonsElem.find('button').on('click', function(){
|
|
// Add criterion on apply
|
|
if($(this).attr('name') === 'apply')
|
|
{
|
|
me.elements.more_criterion.find('.sfm_field input[type="checkbox"]:checked').each(function(iIdx, oElem){
|
|
var sFieldRef = $(oElem).closest('.sfm_field').attr('data-field-ref');
|
|
var oData = {
|
|
'ref': sFieldRef,
|
|
'init_opened': false,
|
|
};
|
|
|
|
me.element.search_form_handler_history('setLatest', sFieldRef);
|
|
me._addCriteria(oData);
|
|
});
|
|
|
|
me._refreshRecentlyUsed();
|
|
me._closeMoreCriterion();
|
|
}
|
|
|
|
// Clear all
|
|
// - Checkboxes
|
|
me.elements.more_criterion.find('.sfm_field input[type="checkbox"]:checked').prop('checked', false);
|
|
// - Filter
|
|
oFilterElem.find('input')
|
|
.val('')
|
|
.trigger('focus');
|
|
|
|
// Hide buttons
|
|
oButtonsElem.hide();
|
|
});
|
|
},
|
|
// - Prepare "submit" button
|
|
_prepareSubmitButton: function()
|
|
{
|
|
var me = this;
|
|
|
|
// DOM
|
|
this.elements.submit_button = $('<div></div>')
|
|
.addClass('sf_button')
|
|
.addClass('sf_submit')
|
|
.appendTo(this.elements.criterion_area.find('.sf_criterion_row:first'));
|
|
|
|
var sButtonText = (this.options.auto_submit === true) ? Dict.S('UI:Button:Refresh') : Dict.S('UI:Button:Search');
|
|
var sButtonIcon = (this.options.auto_submit === true) ? 'fa-refresh' : 'fa-search';
|
|
var oButtonElem = $('<div class="sfb_header"></div>')
|
|
.append('<a class="fa fa-fw ' + sButtonIcon + '" title="' + sButtonText + '" href="#"></a>')
|
|
.appendTo(this.elements.submit_button);
|
|
|
|
// Bind events
|
|
// - Add one criteria
|
|
this.elements.submit_button.on('click', function(oEvent){
|
|
// Prevent anchor
|
|
oEvent.preventDefault();
|
|
|
|
me._onSubmitClick();
|
|
});
|
|
},
|
|
// - Prepare existing criterion
|
|
_prepareExistingCriterion: function()
|
|
{
|
|
// - OR conditions
|
|
var iORCount = 0;
|
|
var aORs = (this.options.search.criterion['or'] !== undefined) ? this.options.search.criterion['or'] : [];
|
|
for(var iORIdx in aORs)
|
|
{
|
|
if(this.elements.criterion_area.find('.sf_criterion_row:nth-of-type(' + (iORCount+1) + ')').length > 0)
|
|
{
|
|
var oCriterionRowElem = this.elements.criterion_area.find('.sf_criterion_row:nth-of-type(' + (iORCount+1) + ')');
|
|
}
|
|
else
|
|
{
|
|
var oCriterionRowElem = $('<div></div>')
|
|
.addClass('sf_criterion_row')
|
|
.appendTo(this.elements.criterion_area);
|
|
}
|
|
|
|
if(oCriterionRowElem.find('.sf_criterion_group').length > 0)
|
|
{
|
|
var oCriterionGroupElem = oCriterionRowElem.find('.sf_criterion_group');
|
|
}
|
|
else
|
|
{
|
|
var oCriterionGroupElem = $('<div></div>')
|
|
.addClass('sf_criterion_group')
|
|
.appendTo(oCriterionRowElem);
|
|
}
|
|
|
|
var aANDs = (aORs[iORIdx]['and'] !== undefined) ? aORs[iORIdx]['and'] : [];
|
|
var aANDsStringified = [];//used in order to deduplicate the crterions
|
|
|
|
|
|
for(var iANDIdx in aANDs)
|
|
{
|
|
var oCriteriaData = aANDs[iANDIdx];
|
|
|
|
var sCriteriaData = JSON.stringify(oCriteriaData);
|
|
|
|
if (aANDsStringified.indexOf(sCriteriaData) == -1)
|
|
{
|
|
aANDsStringified.push(sCriteriaData);
|
|
this._addCriteria(oCriteriaData, oCriterionGroupElem);
|
|
}
|
|
}
|
|
|
|
iORCount++;
|
|
}
|
|
},
|
|
// - Prepare results area
|
|
_prepareResultsArea: function()
|
|
{
|
|
var me = this;
|
|
|
|
var oResultAreaElem;
|
|
|
|
// Build area element
|
|
if(this.options.result_list_outer_selector !== null && $(this.options.result_list_outer_selector).length > 0)
|
|
{
|
|
oResultAreaElem = $(this.options.result_list_outer_selector);
|
|
}
|
|
else
|
|
{
|
|
// Reusing previously created DOM element
|
|
if(this.element.closest('.display_block').parent().find('.sf_results_area').length > 0)
|
|
{
|
|
oResultAreaElem = this.element.closest('.display_block').parent().find('.sf_results_area');
|
|
}
|
|
else
|
|
{
|
|
oResultAreaElem = $('<div class="display_block"></div>').insertAfter(this.element.closest('.display_block'));
|
|
}
|
|
}
|
|
oResultAreaElem.addClass('sf_results_area');
|
|
|
|
// Make placeholder if nothing yet
|
|
if(oResultAreaElem.html() === '')
|
|
{
|
|
// TODO: Make a good UI for this POC.
|
|
// TODO: Translate sentence.
|
|
oResultAreaElem.html('<div class="sf_results_placeholder"><p>Add some criterion on the search box or click the search button to view the objects.</p><p><button type="button">Search<span class="fa fa-search"></span></button></p></div>');
|
|
oResultAreaElem.find('button').on('click', function(){
|
|
// TODO: Bug: Open "Search for CI", change child classe in the dropdown, click the search button. It submit the search for the original child classe, not the current one; whereas a click on the upper right pictogram does. This might be due to the form reloading.
|
|
me._onSubmitClick();
|
|
});
|
|
|
|
if (me.element.find('.search_form_criteria').length == 0)
|
|
{
|
|
me.elements.more_criterion.find('.sfm_header').trigger('click');
|
|
}
|
|
}
|
|
|
|
this.elements.results_area = oResultAreaElem;
|
|
},
|
|
|
|
/**
|
|
* "add new criteria" <li /> markup
|
|
* - with checkbox, label, data-* ...
|
|
* - without event binding
|
|
*
|
|
* @private
|
|
*
|
|
* @param sFieldRef
|
|
* @param aFieldCollectionsEligible
|
|
*
|
|
* @return jQuery detached <li />
|
|
*/
|
|
_getHtmlLiFromFieldRef: function(sFieldRef, aFieldCollectionsEligible) {
|
|
var me = this;
|
|
var oFieldElem = undefined;
|
|
|
|
aFieldCollectionsEligible.forEach(function (sFieldCollection)
|
|
{
|
|
if (typeof me.options.search.fields[sFieldCollection][sFieldRef] == 'undefined')
|
|
{
|
|
return true;//if this field is not present in the Collection, let's try the next
|
|
}
|
|
|
|
var oField = me.options.search.fields[sFieldCollection][sFieldRef];
|
|
var sFieldTitleAttr = (oField.description !== undefined) ? 'title="' + oField.description + '"' : '';
|
|
oFieldElem = $('<li></li>')
|
|
.addClass('sfm_field')
|
|
.attr('data-field-ref', sFieldRef)
|
|
.append('<label ' + sFieldTitleAttr + '><input type="checkbox" value="' + sFieldRef + '" />' + oField.label + '</label>')
|
|
});
|
|
|
|
if (undefined == oFieldElem)
|
|
{
|
|
this._trace('No sFieldRef "' + sFieldRef + '" in given collections', {"aFieldCollectionsEligible":aFieldCollectionsEligible});
|
|
return $('<!-- no sFieldRef in given collection -->');
|
|
}
|
|
|
|
return oFieldElem;
|
|
},
|
|
|
|
|
|
// Criteria helpers
|
|
// - Add a criteria to the form
|
|
_addCriteria: function(oData, oCriterionGroupElem)
|
|
{
|
|
var sRef = oData.ref;
|
|
var sType = sType = (oData.widget !== undefined) ? oData.widget : this._getCriteriaTypeFromFieldRef(sRef);
|
|
|
|
// Force to raw for non removable criteria
|
|
if( (oData.is_removable !== undefined) && (oData.is_removable === false) )
|
|
{
|
|
sType = 'raw';
|
|
}
|
|
|
|
// Add to first OR condition if not specified
|
|
if(oCriterionGroupElem === undefined)
|
|
{
|
|
oCriterionGroupElem = this.elements.criterion_area.find('.sf_criterion_row:first .sf_criterion_group');
|
|
}
|
|
|
|
// Protection against bad initialization data
|
|
if(sType === null)
|
|
{
|
|
this._trace('Could not add criteria as we could not retrieve type for ref "'+sRef+'".');
|
|
return false;
|
|
}
|
|
|
|
// Retrieve widget class
|
|
var sWidgetName = this._getCriteriaWidgetNameFromType(sType);
|
|
|
|
// Add some informations from the field
|
|
if(this._hasFieldDefinition(sRef))
|
|
{
|
|
var oFieldDef = this._getFieldDefinition(sRef);
|
|
oData.field = {
|
|
label: oFieldDef.label,
|
|
class: oFieldDef.class,
|
|
class_alias: oFieldDef.class_alias,
|
|
code: oFieldDef.code,
|
|
has_index: oFieldDef.has_index,
|
|
target_class: oFieldDef.target_class,
|
|
widget: oFieldDef.widget,
|
|
allowed_values: oFieldDef.allowed_values,
|
|
is_null_allowed: oFieldDef.is_null_allowed,
|
|
};
|
|
}
|
|
|
|
// Add widget specific data
|
|
if( (sType === 'date') || (sType === 'date_time') )
|
|
{
|
|
oData.datepicker = this.options.conf_parameters.datepicker;
|
|
}
|
|
if( (sType === 'enum') || (sType === 'external_key') )
|
|
{
|
|
oData.autocomplete = {
|
|
'min_autocomplete_chars': this.options.conf_parameters.min_autocomplete_chars,
|
|
};
|
|
}
|
|
|
|
// Create DOM element
|
|
var oCriteriaElem = $('<div></div>')
|
|
.addClass('sf_criteria')
|
|
.appendTo(oCriterionGroupElem);
|
|
|
|
// Instanciate widget
|
|
$.itop[sWidgetName](oData, oCriteriaElem);
|
|
|
|
return true;
|
|
},
|
|
// - Find a criteria's type from a field's ref (usually <CLASS_ALIAS>.<ATT_CODE>)
|
|
_getCriteriaTypeFromFieldRef: function(sRef)
|
|
{
|
|
// Fallback for unknown widget types or unknown field refs
|
|
var sType = this.options.default_criteria_type;
|
|
|
|
for(var sListIdx in this.options.search.fields)
|
|
{
|
|
if(this.options.search.fields[sListIdx][sRef] !== undefined)
|
|
{
|
|
sType = this.options.search.fields[sListIdx][sRef].widget.toLowerCase();
|
|
break;
|
|
}
|
|
}
|
|
|
|
return sType;
|
|
},
|
|
// - Find a criteria's widget name from a criteria's type
|
|
_getCriteriaWidgetNameFromType: function(sType)
|
|
{
|
|
return 'search_form_criteria' + '_' + (($.itop['search_form_criteria_'+sType] !== undefined) ? sType : 'raw');
|
|
},
|
|
// Criteria handlers
|
|
_onCriteriaValueChanged: function(oData)
|
|
{
|
|
this._updateSearch();
|
|
if(this.options.auto_submit === true)
|
|
{
|
|
this._submit(true);
|
|
}
|
|
},
|
|
_onCriteriaRemoved: function(oData)
|
|
{
|
|
this._updateSearch();
|
|
if(this.options.auto_submit === true)
|
|
{
|
|
this._submit();
|
|
}
|
|
},
|
|
_onCriteriaErrorOccured: function(oData)
|
|
{
|
|
this._setErrorMessage(oData);
|
|
},
|
|
|
|
// Field helpers
|
|
_hasFieldDefinition: function(sRef)
|
|
{
|
|
var bFound = false;
|
|
|
|
for(var sListIdx in this.options.search.fields)
|
|
{
|
|
if(this.options.search.fields[sListIdx][sRef] !== undefined)
|
|
{
|
|
bFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return bFound;
|
|
},
|
|
_getFieldDefinition: function(sRef)
|
|
{
|
|
var oFieldDef = null;
|
|
|
|
for(var sListIdx in this.options.search.fields)
|
|
{
|
|
if(this.options.search.fields[sListIdx][sRef] !== undefined)
|
|
{
|
|
oFieldDef = this.options.search.fields[sListIdx][sRef];
|
|
break;
|
|
}
|
|
}
|
|
|
|
return oFieldDef;
|
|
},
|
|
|
|
// Message helper
|
|
_cleanMessageArea: function()
|
|
{
|
|
this.elements.message_area
|
|
.hide()
|
|
.html('')
|
|
.removeClass('message_error');
|
|
},
|
|
_setErrorMessage: function(sMessage)
|
|
{
|
|
this.elements.message_area
|
|
.addClass('message_error')
|
|
.html(sMessage)
|
|
.show();
|
|
},
|
|
|
|
// Button handlers
|
|
_onSubmitClick: function(oEvent)
|
|
{
|
|
|
|
//if there is an opened criteria let's get it's new value before processing
|
|
if (this.elements.criterion_area.find('.sf_criteria.opened').length > 0)
|
|
{
|
|
this.elements.criterion_area.find('.sf_criteria.opened').trigger('itop.search.criteria.close');
|
|
setTimeout(this._submit.call(this), 300);
|
|
}
|
|
else
|
|
{
|
|
this._submit();
|
|
}
|
|
},
|
|
|
|
|
|
// Submit handlers
|
|
// - External event callback
|
|
_onSubmit: function()
|
|
{
|
|
this._submit();
|
|
},
|
|
// - Do the submit
|
|
_submit: function(bAbortIfNoChange)
|
|
{
|
|
var me = this;
|
|
|
|
// Data
|
|
// - Regular params
|
|
var oData = {
|
|
'params': JSON.stringify({
|
|
'base_oql': this.options.search.base_oql,
|
|
'criterion': this.options.search.criterion,
|
|
}),
|
|
};
|
|
// - List params (pass through for the server), merge data_config with list_params if present.
|
|
var oListParams = {};
|
|
if(this.options.data_config_list_selector !== null)
|
|
{
|
|
var sExtraParams = $(this.options.data_config_list_selector).data('sExtraParams');
|
|
if(sExtraParams !== undefined)
|
|
{
|
|
oListParams = JSON.parse(sExtraParams);
|
|
}
|
|
}
|
|
$.extend(oListParams, this.options.list_params);
|
|
oData.list_params = JSON.stringify(oListParams);
|
|
|
|
if (true === bAbortIfNoChange)
|
|
{
|
|
if (typeof me.oPreviousAjaxParams == "undefined")
|
|
{
|
|
me.oPreviousAjaxParams = oData.params;
|
|
return;
|
|
}
|
|
|
|
if (me.oPreviousAjaxParams == oData.params)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
me.oPreviousAjaxParams = oData.params;
|
|
|
|
// Abort pending request
|
|
if(this.submit.xhr !== null)
|
|
{
|
|
this.submit.xhr.abort();
|
|
}
|
|
|
|
// Show loader
|
|
this._showLoader();
|
|
this._cleanMessageArea();
|
|
|
|
// Do submit
|
|
this.submit.xhr = $.post(
|
|
this.options.endpoint,
|
|
oData
|
|
)
|
|
.done(function(oResponse, sStatus, oXHR){ me._onSubmitSuccess(oResponse); })
|
|
.fail(function(oResponse, sStatus, oXHR){ me._onSubmitFailure(oResponse, sStatus); })
|
|
.always(function(oResponse, sStatus, oXHR){ me._onSubmitAlways(oResponse); });
|
|
},
|
|
// - Called on form submit successes
|
|
_onSubmitSuccess: function(oData)
|
|
{
|
|
this.elements.results_area.html(oData);
|
|
},
|
|
// - Called on form submit failures
|
|
_onSubmitFailure: function(oData, sStatus)
|
|
{
|
|
if(sStatus === 'abort')
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Fallback message in case the server send back only HTML markup.
|
|
var oErrorElem = $(oData.responseText);
|
|
var sErrorMessage = (oErrorElem.text() !== '') ? oErrorElem.text() : Dict.Format('Error:XHR:Fail', '');
|
|
|
|
this._setErrorMessage(sErrorMessage);
|
|
},
|
|
// - Called after form submits
|
|
_onSubmitAlways: function(oData)
|
|
{
|
|
this._hideLoader();
|
|
},
|
|
|
|
|
|
// Global helpers
|
|
_refreshRecentlyUsed: function()
|
|
{
|
|
me = this;
|
|
|
|
var aHistory = me.element.search_form_handler_history("getHistory");
|
|
var oRecentsItemsElem = me.element.find('.sf_list_recents .sfl_items');
|
|
|
|
if (aHistory.length == 0)
|
|
{
|
|
return;
|
|
}
|
|
oRecentsItemsElem.empty();
|
|
|
|
|
|
aHistory.forEach(function(sFieldRef) {
|
|
var oFieldElem = me._getHtmlLiFromFieldRef(sFieldRef, ['zlist', 'others']);
|
|
oRecentsItemsElem.append(oFieldElem);
|
|
});
|
|
|
|
},
|
|
// - Show loader
|
|
_showLoader: function()
|
|
{
|
|
this.elements.results_area.block();
|
|
},
|
|
// - Hide loader
|
|
_hideLoader: function()
|
|
{
|
|
this.elements.results_area.unblock();
|
|
},
|
|
// - Converts a snake_case string to CamelCase
|
|
_toCamelCase: function(sString)
|
|
{
|
|
var aParts = sString.split('_');
|
|
|
|
for(var i in aParts)
|
|
{
|
|
aParts[i] = aParts[i].charAt(0).toUpperCase() + aParts[i].substr(1);
|
|
}
|
|
|
|
return aParts.join('');
|
|
},
|
|
// - Extract sParameter from sUrl
|
|
_extractURLParameter: function(sUrl, sParameter) {
|
|
//prefer to use l.search if you have a location/link object
|
|
var urlparts= sUrl.split('?');
|
|
if (urlparts.length>=2) {
|
|
|
|
var prefix = [
|
|
sParameter+'=',
|
|
encodeURIComponent(sParameter)+'='
|
|
];
|
|
var pars = urlparts[1].split(/[&;]/g);
|
|
|
|
for (var i = 0; i < pars.length; i++) {
|
|
for (var j = 0; j < prefix.length; j++) {
|
|
var pos = pars[i].lastIndexOf(prefix[j], 0);
|
|
if (pos !== -1) {
|
|
return pars[i].substring(pos + prefix[j].length);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return '';
|
|
},
|
|
|
|
|
|
// Debug helpers
|
|
// - Show a trace in the javascript console
|
|
_trace: function(sMessage, oData)
|
|
{
|
|
if(window.console)
|
|
{
|
|
if(oData !== undefined)
|
|
{
|
|
console.log('Search form handler: ' + sMessage, oData);
|
|
}
|
|
else
|
|
{
|
|
console.log('Search form handler: ' + sMessage);
|
|
}
|
|
}
|
|
},
|
|
// - Show current options
|
|
showOptions: function()
|
|
{
|
|
this._trace('Options', this.options);
|
|
}
|
|
});
|
|
});
|