bsselectobjectfieldrenderer.class.inc.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. <?php
  2. // Copyright (C) 2010-2016 Combodo SARL
  3. //
  4. // This file is part of iTop.
  5. //
  6. // iTop is free software; you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // iTop is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with iTop. If not, see <http://www.gnu.org/licenses/>
  18. namespace Combodo\iTop\Renderer\Bootstrap\FieldRenderer;
  19. use \Exception;
  20. use \CoreException;
  21. use \utils;
  22. use \IssueLog;
  23. use \Dict;
  24. use \UserRights;
  25. use \InlineImage;
  26. use \DBObjectSet;
  27. use \MetaModel;
  28. use \Combodo\iTop\Renderer\FieldRenderer;
  29. use \Combodo\iTop\Renderer\RenderingOutput;
  30. use \Combodo\iTop\Form\Field\SelectObjectField;
  31. /**
  32. * Description of BsSelectObjectFieldRenderer
  33. *
  34. * @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
  35. */
  36. class BsSelectObjectFieldRenderer extends FieldRenderer
  37. {
  38. /**
  39. * Returns a RenderingOutput for the FieldRenderer's Field
  40. *
  41. * @return \Combodo\iTop\Renderer\RenderingOutput
  42. */
  43. public function Render()
  44. {
  45. $oOutput = new RenderingOutput();
  46. $sFieldValueClass = $this->oField->GetSearch()->GetClass();
  47. $sFieldMandatoryClass = ($this->oField->GetMandatory()) ? 'form_mandatory' : '';
  48. $sFieldContainerClass = ($this->oField->IsHorizontalDisplayMode() && !$this->oField->GetHidden()) ? 'row' : '';
  49. $iFieldControlType = $this->oField->GetControlType();
  50. // TODO : Remove this when hierarchical search supported
  51. $this->oField->SetHierarchical(false);
  52. // Rendering field in edition mode
  53. if (!$this->oField->GetReadOnly() && !$this->oField->GetHidden())
  54. {
  55. // Rendering field
  56. // - Opening container
  57. $oOutput->AddHtml('<div class="form-group form_group_small ' . $sFieldMandatoryClass . ' ' . $sFieldContainerClass . '">');
  58. // Label
  59. if($this->oField->IsHorizontalDisplayMode()){ $oOutput->AddHtml('<div class="col-sm-3">'); }
  60. if ($this->oField->GetLabel() !== '')
  61. {
  62. $oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">')->AddHtml($this->oField->GetLabel(), true)->AddHtml('</label>');
  63. }
  64. if($this->oField->IsHorizontalDisplayMode()){ $oOutput->AddHtml('</div>'); }
  65. // Value
  66. if($this->oField->IsHorizontalDisplayMode()){ $oOutput->AddHtml('<div class="col-sm-9">'); }
  67. $oOutput->AddHtml('<div class="help-block"></div>');
  68. // - As a select
  69. // TODO : This should be changed when we do the radio button display. For now we display everything with select
  70. //if ($iFieldControlType === SelectObjectField::CONTROL_SELECT)
  71. if (true)
  72. {
  73. // Checking if regular select or autocomplete
  74. $oSearch = $this->oField->GetSearch()->DeepClone();
  75. $oCountSet = new DBObjectSet($oSearch);
  76. $iSetCount = $oCountSet->Count();
  77. // Note : Autocomplete/Search is disabled for template fields as they are not external keys, thus they will just be displayed as regular select.
  78. $bRegularSelect = ( ($iSetCount <= $this->oField->GetMaximumComboLength()) || ($this->oField->GetSearchEndpoint() === null) || ($this->oField->GetSearchEndpoint() === '') );
  79. unset($oCountSet);
  80. $bRegularSelect=false;
  81. // - For regular select
  82. if ($bRegularSelect)
  83. {
  84. // HTML for select part
  85. // - Opening row
  86. $oOutput->AddHtml('<div class="row">');
  87. // - Rendering select
  88. $oOutput->AddHtml('<div class="col-xs-' . ( $this->oField->GetHierarchical() ? 10 : 12 ) . ' col-sm-' . ( $this->oField->GetHierarchical() ? 9 : 12 ) . ' col-md-' . ( $this->oField->GetHierarchical() ? 10 : 12 ) . '">');
  89. $oOutput->AddHtml('<select id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" class="form-control">');
  90. $oOutput->AddHtml('<option value="">')->AddHtml(Dict::S('UI:SelectOne'), false)->AddHtml('</option>');
  91. // - Retrieving choices
  92. $oChoicesSet = new DBObjectSet($oSearch);
  93. while ($oChoice = $oChoicesSet->Fetch())
  94. {
  95. // Note : The test is a double equal on purpose as the type of the value received from the XHR is not always the same as the type of the allowed values. (eg : string vs int)
  96. $sSelectedAtt = ($this->oField->GetCurrentValue() == $oChoice->GetKey()) ? 'selected' : '';
  97. $oOutput->AddHtml('<option value="' . $oChoice->GetKey() . '" ' . $sSelectedAtt . ' >')->AddHtml($oChoice->GetName(), false)->AddHtml('</option>');
  98. }
  99. unset($oChoicesSet);
  100. $oOutput->AddHtml('</select>');
  101. $oOutput->AddHtml('</div>');
  102. // - Closing col for autocomplete & opening col for hierarchy, rendering hierarchy button, closing col and row
  103. $oOutput->AddHtml('<div class="col-xs-' . ( $this->oField->GetHierarchical() ? 2 : 0 ) . ' col-sm-' . ( $this->oField->GetHierarchical() ? 3 : 0 ) . ' col-md-' . ( $this->oField->GetHierarchical() ? 2 : 0 ) . ' text-right">');
  104. $this->RenderHierarchicalSearch($oOutput);
  105. $oOutput->AddHtml('</div>');
  106. // - Closing row
  107. $oOutput->AddHtml('</div>');
  108. // JS FieldChange trigger (:input are not always at the same depth)
  109. $oOutput->AddJs(
  110. <<<EOF
  111. $("#{$this->oField->GetGlobalId()}").off("change keyup").on("change keyup", function(){
  112. var me = this;
  113. $(this).closest(".field_set").trigger("field_change", {
  114. id: $(me).attr("id"),
  115. name: $(me).closest(".form_field").attr("data-field-id"),
  116. value: $(me).val()
  117. });
  118. });
  119. EOF
  120. );
  121. // Attaching JS widget
  122. $oOutput->AddJs(
  123. <<<EOF
  124. $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").portal_form_field({
  125. 'validators': {$this->GetValidatorsAsJson()}
  126. });
  127. EOF
  128. );
  129. }
  130. // - For autocomplete
  131. else
  132. {
  133. $sAutocompleteFieldId = 's_ac_' . $this->oField->GetGlobalId();
  134. $sEndpoint = str_replace('-sMode-', 'autocomplete', $this->oField->GetSearchEndpoint());
  135. $sNoResultText = Dict::S('Portal:Autocomplete:NoResult');
  136. // Retrieving field value
  137. if (($this->oField->GetCurrentValue() !== null) && ($this->oField->GetCurrentValue() !== 0) && ($this->oField->GetCurrentValue() !== ''))
  138. {
  139. try
  140. {
  141. // Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated
  142. $oFieldValue = MetaModel::GetObject($sFieldValueClass, $this->oField->GetCurrentValue(), true, true);
  143. }
  144. catch (CoreException $e)
  145. {
  146. IssueLog::Error('Could not retrieve object ' . $sFieldValueClass . '::' . $this->oField->GetCurrentValue() . ' for "' . $this->oField->GetId() . '" field.');
  147. throw new Exception($e->getMessage(), $e->getCode(), $e->getPrevious());
  148. }
  149. $sFieldValue = $oFieldValue->GetName();
  150. }
  151. else
  152. {
  153. $sFieldValue = '';
  154. }
  155. // HTML for autocomplete part
  156. // - Opening input group
  157. $oOutput->AddHtml('<div class="input-group selectobject">');
  158. // - Rendering autocomplete search
  159. $oOutput->AddHtml('<input type="text" id="' . $sAutocompleteFieldId . '" name="' . $sAutocompleteFieldId . '" value="')->AddHtml($sFieldValue)->AddHtml('" data-target-id="' . $this->oField->GetGlobalId() . ' "class="form-control" />');
  160. $oOutput->AddHtml('<input type="hidden" id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" value="' . $this->oField->GetCurrentValue() . '" />');
  161. // - Rendering buttons
  162. // - Rendering hierarchy button
  163. $this->RenderHierarchicalSearch($oOutput);
  164. // - Rendering regular search
  165. $this->RenderRegularSearch($oOutput);
  166. // - Closing input group
  167. $oOutput->AddHtml('</div>');
  168. // JS FieldChange trigger (:input are not always at the same depth)
  169. // Note : Not used for that field type
  170. // Attaching JS widget
  171. $oOutput->AddJs(
  172. <<<EOF
  173. $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").portal_form_field({
  174. 'validators': {$this->GetValidatorsAsJson()},
  175. 'get_current_value_callback': function(me, oEvent, oData){
  176. var value = null;
  177. value = me.element.find('#{$this->oField->GetGlobalId()}').val();
  178. return value;
  179. },
  180. 'set_current_value_callback': function(me, oEvent, oData){
  181. var sItemId = Object.keys(oData.value)[0];
  182. var sItemName = oData.value[sItemId];
  183. // Updating autocomplete field
  184. me.element.find('#{$this->oField->GetGlobalId()}').val(sItemId);
  185. me.element.find('#{$sAutocompleteFieldId}').val(sItemName);
  186. oAutocompleteSource_{$this->oField->GetId()}.index.datums[sItemId] = {id: sItemId, name: sItemName};
  187. //console.log('callback', oData);
  188. // Triggering field change event
  189. me.element.closest(".field_set").trigger("field_change", {
  190. id: me.element.find('#{$this->oField->GetGlobalId()}').attr("id"),
  191. name: me.element.find('#{$this->oField->GetGlobalId()}').attr("name"),
  192. value: me.element.find('#{$this->oField->GetGlobalId()}').val()
  193. });
  194. }
  195. });
  196. EOF
  197. );
  198. // Preparing JS part for autocomplete
  199. $oOutput->AddJs(
  200. <<<EOF
  201. var oAutocompleteSource_{$this->oField->GetId()} = new Bloodhound({
  202. queryTokenizer: Bloodhound.tokenizers.whitespace,
  203. datumTokenizer: Bloodhound.tokenizers.whitespace,
  204. remote: {
  205. url : '{$sEndpoint}',
  206. prepare: function(query, settings){
  207. settings.type = "POST";
  208. settings.contentType = "application/json; charset=UTF-8";
  209. settings.data = {
  210. sQuery: query,
  211. sFormPath: '{$this->oField->GetFormPath()}',
  212. sFieldId: '{$this->oField->GetId()}',
  213. formmanager_class: $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").closest('.portal_form_handler').portal_form_handler('getOptions').formmanager_class,
  214. formmanager_data: $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").closest('.portal_form_handler').portal_form_handler('getOptions').formmanager_data,
  215. current_values: $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").closest('.portal_form_handler').portal_form_handler('getCurrentValues')
  216. }
  217. return settings;
  218. },
  219. filter: function(response){
  220. var oItems = response.results.items;
  221. // Manualy adding data from remote to the index.datums so we can check data later
  222. for(var sItemKey in oItems)
  223. {
  224. oAutocompleteSource_{$this->oField->GetId()}.index.datums[oItems[sItemKey].id] = oItems[sItemKey];
  225. }
  226. return oItems;
  227. }
  228. }
  229. });
  230. // This check is only for IE9... Otherwise the widget is duplicated on the field causing misbehaviour.
  231. if($('#$sAutocompleteFieldId').typeahead('val') === undefined)
  232. {
  233. $('#$sAutocompleteFieldId').typeahead({
  234. hint: true,
  235. hightlight: true,
  236. minLength: {$this->oField->GetMinAutoCompleteChars()}
  237. },{
  238. name: '{$this->oField->GetId()}',
  239. source: oAutocompleteSource_{$this->oField->GetId()},
  240. limit: 20,
  241. display: 'name',
  242. templates: {
  243. suggestion: Handlebars.compile('<div>{{name}}</div>'),
  244. pending: $("#page_overlay .content_loader").prop('outerHTML'),
  245. notFound: '<div class="no_result">{$sNoResultText}</div>'
  246. }
  247. })
  248. .off('typeahead:select').on('typeahead:select', function(oEvent, oSuggestion){
  249. $('#{$this->oField->GetGlobalId()}').val(oSuggestion.id);
  250. $('#{$sAutocompleteFieldId}').val(oSuggestion.name);
  251. // Triggering set_current_value event
  252. var oValue = {};
  253. oValue[oSuggestion.id] = oSuggestion.name;
  254. $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").trigger('set_current_value', {value: oValue});
  255. })
  256. .off('typeahead:change').on('typeahead:change', function(oEvent, oSuggestion){
  257. // 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
  258. var oDatums = oAutocompleteSource_{$this->oField->GetId()}.index.datums;
  259. var bFound = false;
  260. for(var i in oDatums)
  261. {
  262. if(oDatums[i].name == oSuggestion)
  263. {
  264. bFound = true;
  265. $('#{$this->oField->GetGlobalId()}').val(oDatums[i].id);
  266. $('#{$sAutocompleteFieldId}').val(oDatums[i].name);
  267. break;
  268. }
  269. }
  270. // Emptying the fields if value is incorrect
  271. if(!bFound)
  272. {
  273. $('#{$this->oField->GetGlobalId()}').val(0);
  274. $('#{$sAutocompleteFieldId}').val('');
  275. }
  276. });
  277. }
  278. EOF
  279. );
  280. }
  281. }
  282. if($this->oField->IsHorizontalDisplayMode()){ $oOutput->AddHtml('</div>'); }
  283. // - Closing container
  284. $oOutput->AddHtml('</div>');
  285. }
  286. // ... and in read-only mode (or hidden)
  287. else
  288. {
  289. // Retrieving field value
  290. if ($this->oField->GetCurrentValue() !== null && $this->oField->GetCurrentValue() !== 0 && $this->oField->GetCurrentValue() !== '')
  291. {
  292. // Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated
  293. $oFieldValue = MetaModel::GetObject($sFieldValueClass, $this->oField->GetCurrentValue(), true, true);
  294. $sFieldValue = $oFieldValue->GetName();
  295. }
  296. else
  297. {
  298. $sFieldValue = Dict::S('UI:UndefinedObject');
  299. }
  300. // Opening container
  301. $oOutput->AddHtml('<div class="form-group form_group_small ' . $sFieldContainerClass . '">');
  302. // Showing label / value only if read-only but not hidden
  303. if (!$this->oField->GetHidden())
  304. {
  305. // Label
  306. if($this->oField->IsHorizontalDisplayMode()){ $oOutput->AddHtml('<div class="col-sm-3">'); }
  307. if ($this->oField->GetLabel() !== '')
  308. {
  309. $oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">')->AddHtml($this->oField->GetLabel(), true)->AddHtml('</label>');
  310. }
  311. if($this->oField->IsHorizontalDisplayMode()){ $oOutput->AddHtml('</div>'); }
  312. // Value
  313. if($this->oField->IsHorizontalDisplayMode()){ $oOutput->AddHtml('<div class="col-sm-9">'); }
  314. $oOutput->AddHtml('<div class="form-control-static">' . $sFieldValue . '</div>');
  315. if($this->oField->IsHorizontalDisplayMode()){ $oOutput->AddHtml('</div>'); }
  316. }
  317. // Adding hidden value
  318. $oOutput->AddHtml('<input type="hidden" id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" value="' . $this->oField->GetCurrentValue() . '" class="form-control" />');
  319. // Closing container
  320. $oOutput->AddHtml('</div>');
  321. // JS FieldChange trigger (:input are not always at the same depth)
  322. $oOutput->AddJs(
  323. <<<EOF
  324. $("#{$this->oField->GetGlobalId()}").off("change keyup").on("change keyup", function(){
  325. var me = this;
  326. $(this).closest(".field_set").trigger("field_change", {
  327. id: $(me).attr("id"),
  328. name: $(me).closest(".form_field").attr("data-field-id"),
  329. value: $(me).val()
  330. });
  331. });
  332. EOF
  333. );
  334. // Attaching JS widget
  335. $oOutput->AddJs(
  336. <<<EOF
  337. $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").portal_form_field({
  338. 'validators': {$this->GetValidatorsAsJson()}
  339. });
  340. EOF
  341. );
  342. }
  343. return $oOutput;
  344. }
  345. /**
  346. * Renders an hierarchical search button
  347. *
  348. * @param RenderingOutput $oOutput
  349. */
  350. protected function RenderHierarchicalSearch(RenderingOutput &$oOutput)
  351. {
  352. if ($this->oField->GetHierarchical())
  353. {
  354. $sHierarchicalButtonId = 's_hi_' . $this->oField->GetGlobalId();
  355. $sEndpoint = str_replace('-sMode-', 'hierarchy', $this->oField->GetSearchEndpoint());
  356. $oOutput->AddHtml('<div class="input-group-addon" id="' . $sHierarchicalButtonId . '"><span class="fa fa-sitemap"></span></div>');
  357. $oOutput->AddJs(
  358. <<<EOF
  359. $('#{$sHierarchicalButtonId}').off('click').on('click', function(){
  360. // Creating a new modal
  361. // Note : This could be better if we check for an existing modal first instead of always creating a new one
  362. var oModalElem = $('#modal-for-all').clone();
  363. oModalElem.attr('id', '').attr('data-source-element', '{$sHierarchicalButtonId}').appendTo('body');
  364. // Resizing to small modal
  365. oModalElem.find('.modal-dialog').removeClass('modal-lg').addClass('modal-sm');
  366. // Loading content
  367. oModalElem.find('.modal-content').html($('#page_overlay .overlay_content').html());
  368. oModalElem.find('.modal-content').load(
  369. '{$sEndpoint}',
  370. {
  371. sFormPath: '{$this->oField->GetFormPath()}',
  372. sFieldId: '{$this->oField->GetId()}'
  373. },
  374. function(sResponseText, sStatus, oXHR){
  375. // Hiding modal in case of error as the general AJAX error handler will display a message
  376. if(sStatus === 'error')
  377. {
  378. oModalElem.modal('hide');
  379. }
  380. }
  381. );
  382. oModalElem.modal('show');
  383. });
  384. EOF
  385. );
  386. }
  387. }
  388. /**
  389. * Renders an regular search button
  390. *
  391. * @param RenderingOutput $oOutput
  392. */
  393. protected function RenderRegularSearch(RenderingOutput &$oOutput)
  394. {
  395. $sSearchButtonId = 's_rg_' . $this->oField->GetGlobalId();
  396. $sEndpoint = str_replace('-sMode-', 'from-attribute', $this->oField->GetSearchEndpoint());
  397. $oOutput->AddHtml('<div class="input-group-addon" id="' . $sSearchButtonId . '"><span class="glyphicon glyphicon-search" aria-hidden="true"></span></div>');
  398. $oOutput->AddJs(
  399. <<<EOF
  400. $('#{$sSearchButtonId}').off('click').on('click', function(){
  401. // Creating a new modal
  402. var oModalElem;
  403. if($('.modal[data-source-element="{$sSearchButtonId}"]').length === 0)
  404. {
  405. oModalElem = $('#modal-for-all').clone();
  406. oModalElem.attr('id', '').attr('data-source-element', '{$sSearchButtonId}').appendTo('body');
  407. }
  408. else
  409. {
  410. oModalElem = $('.modal[data-source-element="{$sSearchButtonId}"]').first();
  411. }
  412. // Resizing to small modal
  413. oModalElem.find('.modal-dialog').removeClass('modal-sm').addClass('modal-lg');
  414. // Loading content
  415. oModalElem.find('.modal-content').html($('#page_overlay .overlay_content').html());
  416. oModalElem.find('.modal-content').load(
  417. '{$sEndpoint}',
  418. {
  419. sFormPath: '{$this->oField->GetFormPath()}',
  420. sFieldId: '{$this->oField->GetId()}',
  421. formmanager_class: $(this).closest('.portal_form_handler').portal_form_handler('getOptions').formmanager_class,
  422. formmanager_data: JSON.stringify($(this).closest('.portal_form_handler').portal_form_handler('getOptions').formmanager_data),
  423. current_values: $(this).closest('.portal_form_handler').portal_form_handler('getCurrentValues')
  424. },
  425. function(sResponseText, sStatus, oXHR){
  426. // Hiding modal in case of error as the general AJAX error handler will display a message
  427. if(sStatus === 'error')
  428. {
  429. oModalElem.modal('hide');
  430. }
  431. }
  432. );
  433. oModalElem.modal('show');
  434. });
  435. EOF
  436. );
  437. }
  438. }