diff --git a/application/ui.extkeywidget.class.inc.php b/application/ui.extkeywidget.class.inc.php index d16fb0242..34f7ed506 100644 --- a/application/ui.extkeywidget.class.inc.php +++ b/application/ui.extkeywidget.class.inc.php @@ -63,6 +63,9 @@ require_once(APPROOT.'/application/displayblock.class.inc.php'); class UIExtKeyWidget { + const ENUM_OUTPUT_FORMAT_CSV = 'csv'; + const ENUM_OUTPUT_FORMAT_JSON = 'json'; + protected $iId; protected $sTargetClass; protected $sAttCode; @@ -407,7 +410,7 @@ EOF * @param DBObject $oObj The current object for the OQL context * @param string $sContains The text of the autocomplete to filter the results */ - public function AutoComplete(WebPage $oP, $sFilter, $oObj = null, $sContains) + public function AutoComplete(WebPage $oP, $sFilter, $oObj = null, $sContains, $sOutputFormat = self::ENUM_OUTPUT_FORMAT_CSV) { if (is_null($sFilter)) { @@ -420,9 +423,24 @@ EOF $oValuesSet = new ValueSetObjects($sFilter, 'friendlyname'); // Bypass GetName() to avoid the encoding by htmlentities $oValuesSet->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', $this->bSearchMode); $aValues = $oValuesSet->GetValues(array('this' => $oObj, 'current_extkey_id' => $iCurrentExtKeyId), $sContains); - foreach($aValues as $sKey => $sFriendlyName) + + switch($sOutputFormat) { - $oP->add(trim($sFriendlyName)."\t".$sKey."\n"); + case static::ENUM_OUTPUT_FORMAT_JSON: + // Array flip to preserve values order on the label, otherwise the JS will re-order regarding the keys. + $oP->SetContentType('application/json'); + $oP->add(json_encode(array_flip($aValues))); + break; + + case static::ENUM_OUTPUT_FORMAT_CSV: + foreach($aValues as $sKey => $sFriendlyName) + { + $oP->add(trim($sFriendlyName)."\t".$sKey."\n"); + } + break; + default: + throw new Exception('Invalid output format, "'.$sOutputFormat.'" given.'); + break; } } diff --git a/css/light-grey.css b/css/light-grey.css index 43c2d6491..8a8528ef4 100644 --- a/css/light-grey.css +++ b/css/light-grey.css @@ -942,7 +942,7 @@ input.dp-applied { width: 90px; } .search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operators .sfc_fg_operator.sfc_fg_operator_equals .sfc_op_content input, .search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operators .sfc_fg_operator.sfc_fg_operator_contains .sfc_op_content input, .search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operators .sfc_fg_operator.sfc_fg_operator_starts_with .sfc_op_content input, .search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_operators .sfc_fg_operator.sfc_fg_operator_ends_with .sfc_op_content input { - width: 130px; + width: 160px; } .search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_apply, .search_form_handler .sf_criterion_area .search_form_criteria .sfc_form_group .sfc_fg_cancel { margin-top: 8px; diff --git a/css/light-grey.scss b/css/light-grey.scss index 5f0ba2a2d..c995efd94 100644 --- a/css/light-grey.scss +++ b/css/light-grey.scss @@ -1058,7 +1058,7 @@ input.dp-applied { } .sfc_op_content{ input{ - width: 130px; + width: 160px; } } } diff --git a/js/search/search_form_criteria_enum.js b/js/search/search_form_criteria_enum.js index 641aeb17d..a0910d262 100644 --- a/js/search/search_form_criteria_enum.js +++ b/js/search/search_form_criteria_enum.js @@ -28,6 +28,11 @@ $(function() 'code': null, 'label': Dict.S('Enum:Undefined'), }, + + // Autocomplete + 'autocomplete': { + 'xhr_throttle': 200, + }, }, @@ -85,29 +90,37 @@ $(function() // DOM elements var sOpId = oOpElem.attr('id'); - var oOpContentElem = $('
'); + var oOpContentElem = $('') + .addClass('sfc_opc_multichoices'); // - Check / Uncheck all togglers var sTogglerId = 'toggle_' + sOpId; - var oTogglersElem = $('') + var oTogglersElem = $('') + .addClass('sfc_opc_mc_toggler') .append('') .appendTo(oOpContentElem); // - Filter var sFilterId = 'filter_' + sOpId; var sFilterPlaceholder = (this._hasAutocompleteAllowedValues()) ? Dict.S('UI:Search:Value:Search:Placeholder') : Dict.S('UI:Search:Value:Filter:Placeholder'); - var oFilterElem = $('') - .append('') + var oFilterElem = $('') + .addClass('sf_filter') + .append('') .appendTo(oOpContentElem); // - Allowed values - var oAllowedValuesElem = $('') + var oAllowedValuesElem = $('') + .addClass('sfc_opc_mc_items') .appendTo(oOpContentElem); // - Static values: Always there no matter the field constraints - var oStaticValuesElem = $('') + var oStaticListElem = $('') + .addClass('sfc_opc_mc_items_list') + .addClass('sfc_opc_mc_items_static') .appendTo(oAllowedValuesElem); // - Dynamic values: Depends on the field constraints - var oDynamicValuesElem = $('') + var oDynamicListElem = $('') + .addClass('sfc_opc_mc_items_list') + .addClass('sfc_opc_mc_items_dynamic') .appendTo(oAllowedValuesElem); // - Null value if allowed @@ -118,49 +131,18 @@ $(function() { var sValCode = this.options.null_value.code; var sValLabel = this.options.null_value.label; - var oValueElem = $('') - .addClass('sfc_opc_mc_item') - .attr('data-value-code', sValCode) - .append('') - .appendTo(oStaticValuesElem); - } - - // - Regular allowed values - if (this._hasPreloadedAllowedValues()) - { - var aSortedValues = this._sortValuesByLabel(this._getPreloadedAllowedValues()); - for (var i in aSortedValues) - { - var sValCode = aSortedValues[i][0]; - var sValLabel = aSortedValues[i][1]; - var oValueElem = $('') - .addClass('sfc_opc_mc_item') - .attr('data-value-code', sValCode) - .append('') - .appendTo(oDynamicValuesElem); - - if (this._isSelectedValues(sValCode)) - { - oValueElem.find(':checkbox').prop('checked', true); - } - } - } - - // - Specific to autocomplete mode - if(this._hasAutocompleteAllowedValues()) - { - // Remove filter picto from input - oFilterElem.find('.sff_filter').remove(); - - // Add search dialog button - oFilterElem - .append('') - .addClass('sf_with_buttons'); - - // Prepare "selected" values area + var oValueElem = this._makeListItemElement(sValLabel, sValCode); + oValueElem.appendTo(oStaticListElem); } // Events + // - Filter + oFilterElem.find('.sff_reset').on('click', function(){ + oFilterElem.find('input') + .val('') + .trigger('focus'); + }); + // - Check / Uncheck all toggler oTogglersElem.on('click', function(oEvent){ // Check / uncheck all allowed values @@ -171,14 +153,65 @@ $(function() me._apply(); }); + // - Apply on check + oAllowedValuesElem.on('click', '.sfc_opc_mc_item input', function(oEvent){ + // Prevent propagation, otherwise there will be multiple "_apply()" + oEvent.stopPropagation(); + + // Uncheck toggler + oTogglersElem.find('input:checkbox').prop('checked', false); + + // Apply criteria + me._apply(); + }); + + oOpElem.find('.sfc_op_content').append(oOpContentElem); + + this._prepareInOperatorWithoutAutocomplete(oOpElem, sOpIdx, oOp); + this._prepareInOperatorWithAutocomplete(oOpElem, sOpIdx, oOp); + }, + _prepareInOperatorWithoutAutocomplete: function(oOpElem, sOpIdx, oOp) + { + var me = this; + + if(this._hasAutocompleteAllowedValues()) + { + return false; + } + + var oOpContentElem = oOpElem.find('.sfc_opc_multichoices'); + var oDynamicListElem = oOpElem.find('.sfc_opc_mc_items_dynamic'); + var oFilterElem = oOpElem.find('.sf_filter'); + + // DOM elements + // - Filter + oFilterElem.find('.sff_input_wrapper') + .append(''); + + // - Allowed values + var aSortedValues = this._sortValuesByLabel(this._getPreloadedAllowedValues()); + for (var i in aSortedValues) + { + var sValCode = aSortedValues[i][0]; + var sValLabel = aSortedValues[i][1]; + var oValueElem = this._makeListItemElement(sValLabel, sValCode); + oValueElem.appendTo(oDynamicListElem); + + if (this._isSelectedValues(sValCode)) + { + oValueElem.find(':checkbox').prop('checked', true); + } + } + + // Events // - Filter // Note: "keyup" event is use instead of "keydown", otherwise, the input 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. - var sFilter = $(this).val(); + var sQuery = $(this).val(); - if(sFilter === '') + if(sQuery === '') { oOpContentElem.find('.sfc_opc_mc_item').show(); oFilterElem.find('.sff_filter').show(); @@ -187,7 +220,7 @@ $(function() else { oOpContentElem.find('.sfc_opc_mc_item').each(function(){ - var oRegExp = new RegExp(sFilter, 'ig'); + var oRegExp = new RegExp(sQuery, 'ig'); var sValue = $(this).find('input').val(); var sLabel = $(this).text(); @@ -207,75 +240,84 @@ $(function() 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'); - }); + }, + _prepareInOperatorWithAutocomplete: function(oOpElem, sOpIdx, oOp) + { + var me = this; - // - Apply on check - oAllowedValuesElem.find('.sfc_opc_mc_item input').on('click', function(oEvent){ - // Prevent propagation, otherwise there will be multiple "_apply()" - oEvent.stopPropagation(); - - // Uncheck toggler - oTogglersElem.find('input:checkbox').prop('checked', false); - - // Apply criteria - me._apply(); - }); - - // - Specific to autocomplete mode - // TODO: We NEED to refactor all of this for a better integration. Most might be in the main widget as any widget could use XHR helpers. - if(this._hasAutocompleteAllowedValues()) + if(!this._hasAutocompleteAllowedValues()) { - // // Autocomplete - // var oACXHR = null; - // var oACTimeout = null; - // oFilterElem.find('input').off('keyup focus').on('keyup focus', function(oEvent){ - // // TODO: Move on values with up and down arrow keys; select with space or enter. - // - // var sFilter = $(this).val(); - // - // if(sFilter === '') - // { - // oOpContentElem.find('.sfc_opc_mc_item').html(''); - // oFilterElem.find('.sff_reset').hide(); - // } - // else - // { - // oOpContentElem.find('.sfc_opc_mc_item').html('TOTR: Wait'); - // if(oACXHR !== null) - // { - // oACXHR.abort(); - // } - // oACXHR = $.post( - // AddAppContext(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php'), - // { - // sTargetClass: me.options.field.class, - // //iInputId: me.id, - // //iObjectId: iObjectId, - // sAttCode: me.options.field.code, - // bSearchMode: true, - // operation: 'getObjectName' - // } - // ) - // .done(function(oResponse, sStatus, oXHR){ console.log('ok', oResponse); }) - // .fail(function(oResponse, sStatus, oXHR){ console.log('fail', oResponse); }) - // .always(function(oResponse, sStatus, oXHR){ console.log('always', oResponse); }); - // - // oFilterElem.find('.sff_reset').show(); - // } - // }); - // - // // Open search dialog - // oFilterElem.find('.sff_search_dialog').on('click', function(){ - // // TODO: Open search dialog with right params - // alert('Not implemented yet'); - // }); + return false; } - oOpElem.find('.sfc_op_content').append(oOpContentElem); + var oOpContentElem = oOpElem.find('.sfc_opc_multichoices'); + var oDynamicListElem = oOpElem.find('.sfc_opc_mc_items_dynamic'); + var oFilterElem = oOpElem.find('.sf_filter'); + + // DOM + // - Add search dialog button + oFilterElem + .append('') + .addClass('sf_with_buttons'); + + // - Prepare "selected" values area + var oSelectedElem = $('') + .addClass('sfc_opc_mc_items') + .addClass('sfc_opc_mc_items_selected') + .appendTo(oOpContentElem); + + // Events + // Autocomplete + var oACXHR = null; + var oACTimeout = null; + oFilterElem.find('input').on('keyup focus', function(oEvent){ + // TODO: Move on values with up and down arrow keys; select with space or enter. + + var sQuery = $(this).val(); + + if(sQuery === '') + { + // TODO: Remove items and show placeholder + oDynamicListElem.html('TOTR: Start typing for possible values.'); + + oFilterElem.find('.sff_reset').hide(); + } + else + { + clearTimeout(oACTimeout); + oACTimeout = setTimeout(function(){ + // Show loader + oDynamicListElem.html('TOTR: Please wait...'); + + if(oACXHR !== null) + { + oACXHR.abort(); + } + oACXHR = $.post( + AddAppContext(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php'), + { + sTargetClass: me.options.field.target_class, + sFilter: 'SELECT ' + me.options.field.target_class, + q: sQuery, + bSearchMode: 'true', + sOutputFormat: 'json', + operation: 'ac_extkey', + } + ) + .done(function(oResponse, sStatus, oXHR){ me._onACSearchSuccess(oResponse, oDynamicListElem); }) + .fail(function(oResponse, sStatus, oXHR){ me._onACSearchFail(oResponse, sStatus); }) + .always(function(oResponse, sStatus, oXHR){ me._onACSearchAlways(); }); + + oFilterElem.find('.sff_reset').show(); + }, me.options.autocomplete.xhr_throttle); + } + }); + // + // // Open search dialog + // oFilterElem.find('.sff_search_dialog').on('click', function(){ + // // TODO: Open search dialog with right params + // alert('Not implemented yet'); + // }); }, _setTitle: function(sTitle) { @@ -352,6 +394,32 @@ $(function() }, + // Autocomplete callbacks + _onACSearchSuccess: function(oResponse, oListElem) + { + // TODO: Show results + console.log('ok', oResponse); + for(var iKey in oResponse) + { + var oValueElem = this._makeListItemElement(iKey, oResponse[iKey]); + oValueElem.appendTo(oListElem); + } + + }, + _onACSearchFail: function(oResponse, sStatus) + { + if(sStatus !== 'abort') + { + this.element.find('.sfc_opc_mc_items_dynamic').html('TOTR: Error.'); + } + }, + _onACSearchAlways: function() + { + // TODO: Remove loader + this._trace('TODO: Remove loader'); + }, + + // Value helpers // - Return true if sValue is among the selected values "codes" _isSelectedValues: function(sValue) @@ -407,6 +475,16 @@ $(function() }); return aSortable; - } + }, + // - Make a jQuery element for a list item + _makeListItemElement: function(sLabel, sValue) + { + var oItemElem = $('') + .addClass('sfc_opc_mc_item') + .attr('data-value-code', sValue) + .append(''); + + return oItemElem; + }, }); }); diff --git a/js/search/search_form_handler.js b/js/search/search_form_handler.js index 3d34357a3..129ab6946 100644 --- a/js/search/search_form_handler.js +++ b/js/search/search_form_handler.js @@ -709,6 +709,7 @@ $(function() class: oFieldDef.class, class_alias: oFieldDef.class_alias, code: oFieldDef.code, + target_class: oFieldDef.target_class, widget: oFieldDef.widget, allowed_values: oFieldDef.allowed_values, is_null_allowed: oFieldDef.is_null_allowed, diff --git a/pages/ajax.render.php b/pages/ajax.render.php index 598630070..df3a17236 100644 --- a/pages/ajax.render.php +++ b/pages/ajax.render.php @@ -422,6 +422,7 @@ try $sJson = utils::ReadParam('json', '', false, 'raw_data'); $sContains = utils::ReadParam('q', '', false, 'raw_data'); $bSearchMode = (utils::ReadParam('bSearchMode', 'false') == 'true'); + $sOutputFormat = utils::ReadParam('sOutputFormat', UIExtKeyWidget::ENUM_OUTPUT_FORMAT_CSV, false, 'raw_data'); if ($sContains != '') { if (!empty($sJson)) @@ -435,7 +436,7 @@ try $oObj = null; } $oWidget = new UIExtKeyWidget($sTargetClass, $iInputId, '', $bSearchMode); - $oWidget->AutoComplete($oPage, $sFilter, $oObj, $sContains); + $oWidget->AutoComplete($oPage, $sFilter, $oObj, $sContains, $sOutputFormat); } break;