//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, /* Submit the search form automatically on criteria change */ 'auto_submit': true, /* Submit the search form when the page is first loaded */ 'submit_on_load': true, 'show_obsolete_data': 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, /** @var {ScrollMagic.Controller} SM controller for the sticky header */ sticky_header_controller: 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(); // - Sticky header this._updateStickyHeaderHandler(); // 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, }); // If auto submit is enabled, also submit on first display if (this.options.auto_submit === true && this.options.submit_on_load === true) { this._submit(); } }, // called when created, and later when changing options _refresh: function() { this._updateStickyHeaderHandler(); }, // events bound via _bind are removed automatically // revert other modifications here _destroy: function() { // Remove ScrollMagic controller, typicaly useful when the search form is loaded to display one for another class if(this.sticky_header_controller !== null) { this.sticky_header_controller.destroy(true) } 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; } var sNewUrl = GetAbsoluteUrlAppRoot()+'pages/UI.php?operation=search'; sNewUrl = sNewUrl + '&filter='+encodeURI(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); } $('#ibo-breadcrumbs') .breadcrumbs('destroy') .breadcrumbs({ 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'], 'icon_type': oData['breadcrumb_icon_type'], 'description': '' } }); }); // Refresh handler when the list has changed // - Initialization // - Destroy / reinitialization (changing the DM class of the search form) this.element.scrollParent().on('init.dt', function(oEvent) { me._updateStickyHeaderHandler(); }); // Refresh sticky positions when results are redrawn // - AJAX pagination, filtering // - Page length changes this.element.scrollParent().on('draw.dt column-sizing.dt', function(oEvent) { me._updateStickyPositions(); }); // Refresh handler when resising: // - The window // - The search form when the numerous criteria wrap on a new line if(window.ResizeObserver) { const oPanelRO = new ResizeObserver(function(){ me._updateStickyPositions(); }); oPanelRO.observe(this.element[0]); } }, // - Update search option of the widget _updateSearch: function() { var me = this; // Criterion var oCriterion = { 'or': [{ 'and': [] }] }; // - Retrieve criterion var iCurrentCriterionRow = 0; this.elements.criterion_area.find('.sf_criterion_row').each(function (iDomCriterionRowIdx) { var isFirstRow = (iDomCriterionRowIdx === 0), oCriterionRowElem = $(this), oCriteriaRowCriterias = oCriterionRowElem.find('.search_form_criteria'); if (oCriteriaRowCriterias.length === 0) { if (!isFirstRow) { $(this).remove(); } } else { oCriteriaRowCriterias.each(function () { var oCriteriaData = $(this).triggerHandler('itop.search.criteria.get_data'); if (null != oCriteriaData) { if (!oCriterion['or'][iCurrentCriterionRow]) { oCriterion['or'][iCurrentCriterionRow] = {'and': []}; } oCriterion['or'][iCurrentCriterionRow]['and'].push(oCriteriaData); } else { $(this).remove(); } }); iCurrentCriterionRow++; } }); // - 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().find('input[type="text"]') .val('') .trigger('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'); } // - Show obsolete data option if(this.options.show_obsolete_data === false) { this.element.addClass('hide_obsolete_data'); } // - 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('.ibo-panel--header').on('click', function(oEvent){ // Prevent anchors oEvent.preventDefault(); // Prevent toggle on ') .appendTo(oContentElem); // - Lists container var oListsElem = $('
') .addClass('sfm_lists') .appendTo(oContentElem); // - Recently used list var oRecentsElem = $('
') .addClass('sf_list') .addClass('sf_list_recents') .appendTo(oListsElem); $('
') .text(Dict.S('UI:Search:AddCriteria:List:RecentlyUsed:Title')) .appendTo(oRecentsElem); var oRecentsItemsElem = $('') .append('
  • ' + Dict.S('UI:Search:AddCriteria:List:RecentlyUsed:Placeholder') + '
  • ') .appendTo(oRecentsElem); me._refreshRecentlyUsed(); // - Search zlist list var oZlistElem = $('
    ') .addClass('sf_list') .addClass('sf_list_zlist') .appendTo(oListsElem); $('
    ') .text(Dict.S('UI:Search:AddCriteria:List:MostPopular:Title')) .appendTo(oZlistElem); var oZListItemsElem = $('') .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 = $('
    ') .addClass('sf_list') .addClass('sf_list_others') .appendTo(oListsElem); $('
    ') .text(Dict.S('UI:Search:AddCriteria:List:Others:Title')) .appendTo(oOthersElem); var oOthersItemsElem = $('') .appendTo(oOthersElem); for(var sFieldRef in this.options.search.fields.others) { var oFieldElem = me._getHtmlLiFromFieldRef(sFieldRef, ['others']); oFieldElem.appendTo(oOthersItemsElem); } } // - Buttons var oButtonsElem = $('
    ') .addClass('sfm_buttons') .append('') .append('') .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 = $('
    ') .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) ? 'fas fa-sync-alt' : 'fas fa-search'; var oButtonElem = $('
    ') .append('') .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 = $('
    ') .addClass('sf_criterion_row ibo-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 = $('
    ') .addClass('sf_criterion_group ibo-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 = $('
    ').insertAfter(this.element.closest('.display_block')); } } // Make placeholder if nothing yet if(oResultAreaElem.html() === '') { oResultAreaElem.html('

    ' + Dict.S('UI:Search:NoAutoSubmit:ExplainText') + '

    '); 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"
  • markup * - with checkbox, label, data-* ... * - without event binding * * @private * * @param sFieldRef * @param aFieldCollectionsEligible * * @return jQuery detached
  • */ _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 = $('
  • ') .addClass('sfm_field') .attr('data-field-ref', sFieldRef) .append('') }); if (undefined == oFieldElem) { this._trace('No sFieldRef "' + sFieldRef + '" in given collections', {"aFieldCollectionsEligible":aFieldCollectionsEligible}); return $(''); } 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().find('.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 = $('
    ') .addClass('sf_criteria') .appendTo(oCriterionGroupElem); // Instanciate widget $.itop[sWidgetName](oData, oCriteriaElem); return true; }, // - Find a criteria's type from a field's ref (usually .) _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) && (oData.had_values === 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); if (me.element.parents('.ui-dialog').length !== 0) { oListParams.update_history = false; } 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(); } // Remove sticky state as we want to return at the beginning of the results this._exitStickyState(); // 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(); }, //--------------------------- // Sticky header helpers //--------------------------- /** * Main function for the sticky header, it creates or recreates all the mechanism (viewport, trigger points, sizing/positioning of fixed elements) * Must be called whenever the reference elements (viewport, trigger elements, ...) change to ensure that everything is well positioned. * * @return {void} * @private */ _updateStickyHeaderHandler: function () { const me = this; // Update the reference viewport this.options.viewport_elem = this.element.scrollParent()[0]; // Clean SM controller if there was already one if (null !== this.sticky_header_controller) { this.sticky_header_controller.destroy(true); } // Prepare SM controller this.sticky_header_controller = new ScrollMagic.Controller({ container: this.options.viewport_elem, }); this._addScrollSceneForForm(); this._addScrollSceneForResults(); }, /** * Observe when the search form reaches/leaves the viewport's top * @private */ _addScrollSceneForForm: function() { const me = this; const oFormPanelHeaderElem = this._getFormPanelHeaderElem(); new ScrollMagic.Scene({ triggerElement: oFormPanelHeaderElem[0], triggerHook: 0, offset: oFormPanelHeaderElem.outerHeight(), }) .on('enter', function () { me._onFormBecomesSticky(); }) .on('leave', function () { me._onFormStopsBeingSticky(); }) .addTo(this.sticky_header_controller); }, /** * Callback for the form element SM Scene * @return {void} * @private */ _onFormBecomesSticky: function() { this._getFormPanelBodyElem().addClass('ibo-is-sticking'); this._updateStickyPositions(); }, /** * Callback for the form element SM Scene * @return {void} * @private */ _onFormStopsBeingSticky: function() { this._getFormPanelBodyElem().removeClass('ibo-is-sticking'); this._updateStickyPositions(); }, /** * Observer when the results' top toolbar reaches/leaves the search form's bottom * @private */ _addScrollSceneForResults: function() { const me = this; const oFormPanelHeaderElem = this._getFormPanelHeaderElem(); const oResultsPanelBodyElem = this._getResultsPanelElem().find('.ibo-panel--body').first(); // Ensure result body panel has been created if (oResultsPanelBodyElem.length === 0) { return; } // Note: As offset() starts from the very top of the window, we need to take into account the top container height! let fOffset = oResultsPanelBodyElem.offset().top - $('#ibo-top-container').outerHeight() - this._getFormPanelBodyElem().outerHeight(); if (this._isInAModal()) { fOffset = fOffset - this.element.closest('[role="dialog"]').offset().top; } new ScrollMagic.Scene({ triggerElement: oFormPanelHeaderElem[0], triggerHook: 0, // Careful, this won't get updated dynamically, meaning that if the elements move or resize, it won't be exact anymore offset: fOffset, }) .on('enter', function () { me._onResultsBecomesSticky(); }) .on('leave', function () { me._onResultsStopsBeingSticky(); }) .addTo(this.sticky_header_controller); }, /** * Callback for the results element SM Scene * @return {void} * @private */ _onResultsBecomesSticky: function() { this._getResultsPanelElem().addClass('ibo-is-sticking'); this._getResultsToolbarTopElem().addClass('ibo-is-sticking'); this._getResultsTableHeaders().addClass('ibo-is-sticking'); this._updateStickyPositions(); }, /** * Callback for the results element SM Scene * @return {void} * @private */ _onResultsStopsBeingSticky: function() { this._getResultsPanelElem().removeClass('ibo-is-sticking'); this._getResultsToolbarTopElem().removeClass('ibo-is-sticking'); this._getResultsTableHeaders().removeClass('ibo-is-sticking'); this._updateStickyPositions(); }, /** * Update all the concerned elements position / size * * @param bImmediate {boolean} Set to true if the update of the positions should have a small delay. This can be useful when ahving CSS transitions that needs to be done before computing positions. * @private */ _updateStickyPositions: function(bImmediate = true) { const me = this; if(!bImmediate) { setTimeout(function() { me._updateStickyPositions(true); }, 300); return; } // Update the sticky elements positions this._updateFormPosition(); this._updateResultsToolbarTopPosition(); this._updateResultsTableHeadersPosition(); // Update the scrolling element's top padding to avoid having a visual glitch when the results panel elements becomes sticky and changes the result table vertical position // Note: The initial "-8" offset is there because we don't know yet how to retrieve the results panel body :before height, therefore this will not work well with custom themes... 😕 const iInitialOffset = -8; let iResultsPanelOffset = iInitialOffset; const aStickableElems = [ this._getFormPanelBodyElem(), this._getResultsToolbarTopElem(), this._getResultsTableHeaders() ]; for(let oElem of aStickableElems){ if(oElem.hasClass('ibo-is-sticking')){ iResultsPanelOffset += parseInt(oElem.outerHeight() + parseInt(oElem.css('margin-top')) + parseInt(oElem.css('margin-bottom'))); } } // If computed offset is the same as the initial, we should reset the padding. if(iInitialOffset === iResultsPanelOffset) { this._getResultsPanelElem().css('padding-top', ''); } else { this._getResultsPanelElem().css('padding-top', iResultsPanelOffset); } }, /** * Update only the search form position * @private */ _updateFormPosition: function() { const oFormPanelBodyElem = this._getFormPanelBodyElem(); if(this._isElementSticking(oFormPanelBodyElem)) { const oFormPanelElem = this._getFormPanelElem(); oFormPanelBodyElem.css({ 'top': $(this.options.viewport_elem).offset()?.top, // N°7402 - In case viewport is the document, offset() will return undefined 'left': oFormPanelElem.offset().left, 'width': oFormPanelElem.outerWidth(), }); } else { oFormPanelBodyElem.css({ 'top': '', 'left': '', 'width': '', }); } }, /** * Update only the results top toolbar position * @private */ _updateResultsToolbarTopPosition: function() { if(this._isElementSticking(this._getResultsToolbarTopElem())){ const oFormPanelBodyElem = this._getFormPanelBodyElem(); this._getResultsToolbarTopElem().css({ 'top': oFormPanelBodyElem.offset().top + oFormPanelBodyElem.outerHeight(), 'left': oFormPanelBodyElem.offset().left, 'width': oFormPanelBodyElem.outerWidth(), 'padding-top': this._getResultsToolbarTopElem().css('margin-top'), 'padding-bottom': this._getResultsToolbarTopElem().css('margin-bottom'), }); } else { this._getResultsToolbarTopElem().css({ 'top': '', 'left': '', 'width': '', 'padding-top': '', 'padding-bottom': '', }); } }, /** * Update only the results table headers position * @private */ _updateResultsTableHeadersPosition: function() { if(this._isElementSticking(this._getResultsTableHeaders())){ const oFormPanelElem = this._getFormPanelElem(); const oResultsToolbarTopElem = this._getResultsToolbarTopElem(); this._getResultsTableHeaders().css({ 'top': oResultsToolbarTopElem.offset().top + oResultsToolbarTopElem.outerHeight(), 'left': oFormPanelElem.offset().left, 'width': oFormPanelElem.outerWidth(), 'padding-top': this._getResultsTableHeaders().css('margin-top'), 'padding-bottom': this._getResultsTableHeaders().css('margin-bottom'), }); }else{ this._getResultsTableHeaders().css({ 'top': '', 'left': '', 'width': '', 'padding-top': '', 'padding-bottom': '', }); } }, /** * Exit the sticky state for the whole search, returning to the top of the results * @return {void} */ _exitStickyState: function() { this._onFormStopsBeingSticky(); this._onResultsStopsBeingSticky(); this.element.scrollParent().scrollTop(); }, /** * @param oElem {Object} jQuery object representing the element to test * @return {boolean} True if oElem is currently sticking * @private */ _isElementSticking: function(oElem) { return oElem.closest('.ibo-is-sticking').length > 0; }, /** * @return {Object} The jQuery object representing the search form panel element (where the criteria are) * @private */ _getFormPanelElem: function() { return this.element.closest('.ibo-search-form-panel'); }, /** * @return {null|Object} The jQuery object representing the header of the search form panel; or null if none found * @private */ _getFormPanelHeaderElem: function() { const oFormPanelElem = this._getFormPanelElem(); if(oFormPanelElem.length === 0){ return null; } return oFormPanelElem.find('[data-role="ibo-panel--header"]').first(); }, /** * @return {null|Object} The jQuery object representing the body of the search form panel; or null if none found * @private */ _getFormPanelBodyElem: function() { const oFormPanelElem = this._getFormPanelElem(); if(oFormPanelElem.length === 0){ return null; } return oFormPanelElem.find('[data-role="ibo-panel--body"]').first(); }, /** * @return {Object} The jQuery object representing the complete results panel * @private */ _getResultsPanelElem: function() { return this.elements.results_area === null ? null : this.elements.results_area.find('[data-role="ibo-panel"]').first(); }, /** * @return {Object} The jQuery object representing the top toolbar of the results (pagination, ...) * @private */ _getResultsToolbarTopElem: function() { return this.elements.results_area === null ? null : this.elements.results_area.find('.ibo-datatable--toolbar').first(); }, /** * @return {Object} The jQuery object representing the columns headers of the results * @private */ _getResultsTableHeaders: function() { return this.elements.results_area === null ? null : this.elements.results_area.find('.dataTables_scrollHead').first(); }, //--------------------------- // 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(); }, /** * @return {boolean} True if the search form is in a modal window * @private * @since 3.0.0 */ _isInAModal: function() { return this.element.closest('[role="dialog"]').length > 0; }, // - 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); } }); });