namespace Combodo\iTop\Renderer\Bootstrap\FieldRenderer; use \Exception; use \CoreException; use \utils; use \IssueLog; use \Dict; use \UserRights; use \InlineImage; use \DBObjectSet; use \MetaModel; use \Combodo\iTop\Renderer\FieldRenderer; use \Combodo\iTop\Renderer\RenderingOutput; use \Combodo\iTop\Form\Field\SelectObjectField; /** * Description of BsSelectObjectFieldRenderer * * @author Guillaume Lajarige */ class BsSelectObjectFieldRenderer extends FieldRenderer { /** * Returns a RenderingOutput for the FieldRenderer's Field * * @return \Combodo\iTop\Renderer\RenderingOutput */ public function Render() { $oOutput = new RenderingOutput(); $sFieldValueClass = $this->oField->GetSearch()->GetClass(); $sFieldMandatoryClass = ($this->oField->GetMandatory()) ? 'form_mandatory' : ''; $sFieldContainerClass = ($this->oField->IsHorizontalDisplayMode() && !$this->oField->GetHidden()) ? 'row' : ''; $iFieldControlType = $this->oField->GetControlType(); // TODO : Remove this when hierarchical search supported $this->oField->SetHierarchical(false); // Rendering field in edition mode if (!$this->oField->GetReadOnly() && !$this->oField->GetHidden()) { // Rendering field // - Opening container $oOutput->AddHtml('
'); // Label if($this->oField->IsHorizontalDisplayMode()){ $oOutput->AddHtml('
'); } if ($this->oField->GetLabel() !== '') { $oOutput->AddHtml(''); } if($this->oField->IsHorizontalDisplayMode()){ $oOutput->AddHtml('
'); } // Value if($this->oField->IsHorizontalDisplayMode()){ $oOutput->AddHtml('
'); } $oOutput->AddHtml('
'); // - As a select // TODO : This should be changed when we do the radio button display. For now we display everything with select //if ($iFieldControlType === SelectObjectField::CONTROL_SELECT) if (true) { // Checking if regular select or autocomplete $oSearch = $this->oField->GetSearch()->DeepClone(); $oCountSet = new DBObjectSet($oSearch); $iSetCount = $oCountSet->Count(); // Note : Autocomplete/Search is disabled for template fields as they are not external keys, thus they will just be displayed as regular select. $bRegularSelect = ( ($iSetCount <= $this->oField->GetMaximumComboLength()) || ($this->oField->GetSearchEndpoint() === null) || ($this->oField->GetSearchEndpoint() === '') ); unset($oCountSet); $bRegularSelect=false; // - For regular select if ($bRegularSelect) { // HTML for select part // - Opening row $oOutput->AddHtml('
'); // - Rendering select $oOutput->AddHtml('
'); $oOutput->AddHtml(''); $oOutput->AddHtml('
'); // - Closing col for autocomplete & opening col for hierarchy, rendering hierarchy button, closing col and row $oOutput->AddHtml('
'); $this->RenderHierarchicalSearch($oOutput); $oOutput->AddHtml('
'); // - Closing row $oOutput->AddHtml('
'); // JS FieldChange trigger (:input are not always at the same depth) $oOutput->AddJs( <<oField->GetGlobalId()}").off("change keyup").on("change keyup", function(){ var me = this; $(this).closest(".field_set").trigger("field_change", { id: $(me).attr("id"), name: $(me).closest(".form_field").attr("data-field-id"), value: $(me).val() }); }); EOF ); // Attaching JS widget $oOutput->AddJs( <<GetValidatorsAsJson()} }); EOF ); } // - For autocomplete else { $sAutocompleteFieldId = 's_ac_' . $this->oField->GetGlobalId(); $sEndpoint = str_replace('-sMode-', 'autocomplete', $this->oField->GetSearchEndpoint()); $sNoResultText = Dict::S('Portal:Autocomplete:NoResult'); // Retrieving field value if (($this->oField->GetCurrentValue() !== null) && ($this->oField->GetCurrentValue() !== 0) && ($this->oField->GetCurrentValue() !== '')) { try { // Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated $oFieldValue = MetaModel::GetObject($sFieldValueClass, $this->oField->GetCurrentValue(), true, true); } catch (CoreException $e) { IssueLog::Error('Could not retrieve object ' . $sFieldValueClass . '::' . $this->oField->GetCurrentValue() . ' for "' . $this->oField->GetId() . '" field.'); throw new Exception($e->getMessage(), $e->getCode(), $e->getPrevious()); } $sFieldValue = $oFieldValue->GetName(); } else { $sFieldValue = ''; } // HTML for autocomplete part // - Opening input group $oOutput->AddHtml('
'); // - Rendering autocomplete search $oOutput->AddHtml(''); $oOutput->AddHtml(''); // - Rendering buttons // - Rendering hierarchy button $this->RenderHierarchicalSearch($oOutput); // - Rendering regular search $this->RenderRegularSearch($oOutput); // - Closing input group $oOutput->AddHtml('
'); // JS FieldChange trigger (:input are not always at the same depth) // Note : Not used for that field type // Attaching JS widget $oOutput->AddJs( <<GetValidatorsAsJson()}, 'get_current_value_callback': function(me, oEvent, oData){ var value = null; value = me.element.find('#{$this->oField->GetGlobalId()}').val(); return value; }, 'set_current_value_callback': function(me, oEvent, oData){ var sItemId = Object.keys(oData.value)[0]; var sItemName = oData.value[sItemId]; // Updating autocomplete field me.element.find('#{$this->oField->GetGlobalId()}').val(sItemId); me.element.find('#{$sAutocompleteFieldId}').val(sItemName); oAutocompleteSource_{$this->oField->GetId()}.index.datums[sItemId] = {id: sItemId, name: sItemName}; //console.log('callback', oData); // Triggering field change event me.element.closest(".field_set").trigger("field_change", { id: me.element.find('#{$this->oField->GetGlobalId()}').attr("id"), name: me.element.find('#{$this->oField->GetGlobalId()}').attr("name"), value: me.element.find('#{$this->oField->GetGlobalId()}').val() }); } }); EOF ); // Preparing JS part for autocomplete $oOutput->AddJs( <<oField->GetId()} = new Bloodhound({ queryTokenizer: Bloodhound.tokenizers.whitespace, datumTokenizer: Bloodhound.tokenizers.whitespace, remote: { url : '{$sEndpoint}', prepare: function(query, settings){ settings.type = "POST"; settings.contentType = "application/json; charset=UTF-8"; settings.data = { sQuery: query, sFormPath: '{$this->oField->GetFormPath()}', sFieldId: '{$this->oField->GetId()}', formmanager_class: $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").closest('.portal_form_handler').portal_form_handler('getOptions').formmanager_class, formmanager_data: $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").closest('.portal_form_handler').portal_form_handler('getOptions').formmanager_data, current_values: $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").closest('.portal_form_handler').portal_form_handler('getCurrentValues') } return settings; }, filter: function(response){ var oItems = response.results.items; // Manualy adding data from remote to the index.datums so we can check data later for(var sItemKey in oItems) { oAutocompleteSource_{$this->oField->GetId()}.index.datums[oItems[sItemKey].id] = oItems[sItemKey]; } return oItems; } } }); // This check is only for IE9... Otherwise the widget is duplicated on the field causing misbehaviour. if($('#$sAutocompleteFieldId').typeahead('val') === undefined) { $('#$sAutocompleteFieldId').typeahead({ hint: true, hightlight: true, minLength: {$this->oField->GetMinAutoCompleteChars()} },{ name: '{$this->oField->GetId()}', source: oAutocompleteSource_{$this->oField->GetId()}, limit: 20, display: 'name', templates: { suggestion: Handlebars.compile('
{{name}}
'), pending: $("#page_overlay .content_loader").prop('outerHTML'), notFound: '
{$sNoResultText}
' } }) .off('typeahead:select').on('typeahead:select', function(oEvent, oSuggestion){ $('#{$this->oField->GetGlobalId()}').val(oSuggestion.id); $('#{$sAutocompleteFieldId}').val(oSuggestion.name); // Triggering set_current_value event var oValue = {}; oValue[oSuggestion.id] = oSuggestion.name; $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").trigger('set_current_value', {value: oValue}); }) .off('typeahead:change').on('typeahead:change', function(oEvent, oSuggestion){ // Checking if the value is a correct value. This is necessary because the user could empty the field / remove some chars and typeahead would not update the hidden input var oDatums = oAutocompleteSource_{$this->oField->GetId()}.index.datums; var bFound = false; for(var i in oDatums) { if(oDatums[i].name == oSuggestion) { bFound = true; $('#{$this->oField->GetGlobalId()}').val(oDatums[i].id); $('#{$sAutocompleteFieldId}').val(oDatums[i].name); break; } } // Emptying the fields if value is incorrect if(!bFound) { $('#{$this->oField->GetGlobalId()}').val(0); $('#{$sAutocompleteFieldId}').val(''); } }); } EOF ); } } if($this->oField->IsHorizontalDisplayMode()){ $oOutput->AddHtml('
'); } // - Closing container $oOutput->AddHtml('
'); } // ... and in read-only mode (or hidden) else { // Retrieving field value if ($this->oField->GetCurrentValue() !== null && $this->oField->GetCurrentValue() !== 0 && $this->oField->GetCurrentValue() !== '') { // Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated $oFieldValue = MetaModel::GetObject($sFieldValueClass, $this->oField->GetCurrentValue(), true, true); $sFieldValue = $oFieldValue->GetName(); } else { $sFieldValue = Dict::S('UI:UndefinedObject'); } // Opening container $oOutput->AddHtml('
'); // Showing label / value only if read-only but not hidden if (!$this->oField->GetHidden()) { // Label if($this->oField->IsHorizontalDisplayMode()){ $oOutput->AddHtml('
'); } if ($this->oField->GetLabel() !== '') { $oOutput->AddHtml(''); } if($this->oField->IsHorizontalDisplayMode()){ $oOutput->AddHtml('
'); } // Value if($this->oField->IsHorizontalDisplayMode()){ $oOutput->AddHtml('
'); } $oOutput->AddHtml('
' . $sFieldValue . '
'); if($this->oField->IsHorizontalDisplayMode()){ $oOutput->AddHtml('
'); } } // Adding hidden value $oOutput->AddHtml(''); // Closing container $oOutput->AddHtml('
'); // JS FieldChange trigger (:input are not always at the same depth) $oOutput->AddJs( <<oField->GetGlobalId()}").off("change keyup").on("change keyup", function(){ var me = this; $(this).closest(".field_set").trigger("field_change", { id: $(me).attr("id"), name: $(me).closest(".form_field").attr("data-field-id"), value: $(me).val() }); }); EOF ); // Attaching JS widget $oOutput->AddJs( <<GetValidatorsAsJson()} }); EOF ); } return $oOutput; } /** * Renders an hierarchical search button * * @param RenderingOutput $oOutput */ protected function RenderHierarchicalSearch(RenderingOutput &$oOutput) { if ($this->oField->GetHierarchical()) { $sHierarchicalButtonId = 's_hi_' . $this->oField->GetGlobalId(); $sEndpoint = str_replace('-sMode-', 'hierarchy', $this->oField->GetSearchEndpoint()); $oOutput->AddHtml('
'); $oOutput->AddJs( <<oField->GetFormPath()}', sFieldId: '{$this->oField->GetId()}' }, function(sResponseText, sStatus, oXHR){ // Hiding modal in case of error as the general AJAX error handler will display a message if(sStatus === 'error') { oModalElem.modal('hide'); } } ); oModalElem.modal('show'); }); EOF ); } } /** * Renders an regular search button * * @param RenderingOutput $oOutput */ protected function RenderRegularSearch(RenderingOutput &$oOutput) { $sSearchButtonId = 's_rg_' . $this->oField->GetGlobalId(); $sEndpoint = str_replace('-sMode-', 'from-attribute', $this->oField->GetSearchEndpoint()); $oOutput->AddHtml('
'); $oOutput->AddJs( <<oField->GetFormPath()}', sFieldId: '{$this->oField->GetId()}', formmanager_class: $(this).closest('.portal_form_handler').portal_form_handler('getOptions').formmanager_class, formmanager_data: JSON.stringify($(this).closest('.portal_form_handler').portal_form_handler('getOptions').formmanager_data), current_values: $(this).closest('.portal_form_handler').portal_form_handler('getCurrentValues') }, function(sResponseText, sStatus, oXHR){ // Hiding modal in case of error as the general AJAX error handler will display a message if(sStatus === 'error') { oModalElem.modal('hide'); } } ); oModalElem.modal('show'); }); EOF ); } }