bsselectobjectfieldrenderer.class.inc.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  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. $iFieldControlType = $this->oField->GetControlType();
  49. // TODO : Remove this when hierarchical search supported
  50. $this->oField->SetHierarchical(false);
  51. // Rendering field in edition mode
  52. if (!$this->oField->GetReadOnly() && !$this->oField->GetHidden())
  53. {
  54. // Rendering field
  55. $oOutput->AddHtml('<div class="form-group ' . $sFieldMandatoryClass . '">');
  56. if ($this->oField->GetLabel() !== '')
  57. {
  58. $oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">')->AddHtml($this->oField->GetLabel(), true)->AddHtml('</label>');
  59. }
  60. $oOutput->AddHtml('<div class="help-block"></div>');
  61. // - As a select
  62. // TODO : This should be changed when we do the radio button display. For now we display everything with select
  63. //if ($iFieldControlType === SelectObjectField::CONTROL_SELECT)
  64. if (true)
  65. {
  66. // Checking if regular select or autocomplete
  67. $oSearch = $this->oField->GetSearch()->DeepClone();
  68. $oCountSet = new DBObjectSet($oSearch);
  69. $iSetCount = $oCountSet->Count();
  70. $bRegularSelect = ($iSetCount <= $this->oField->GetMaximumComboLength());
  71. unset($oCountSet);
  72. // - For regular select
  73. if ($bRegularSelect)
  74. {
  75. // HTML for select part
  76. // - Opening row
  77. $oOutput->AddHtml('<div class="row">');
  78. // - Rendering select
  79. $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 ) . '">');
  80. $oOutput->AddHtml('<select id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" class="form-control">');
  81. $oOutput->AddHtml('<option value="">')->AddHtml(Dict::S('UI:SelectOne'), false)->AddHtml('</option>');
  82. // - Retrieving choices
  83. $oChoicesSet = new DBObjectSet($oSearch);
  84. while ($oChoice = $oChoicesSet->Fetch())
  85. {
  86. // 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)
  87. $sSelectedAtt = ($this->oField->GetCurrentValue() == $oChoice->GetKey()) ? 'selected' : '';
  88. $oOutput->AddHtml('<option value="' . $oChoice->GetKey() . '" ' . $sSelectedAtt . ' >')->AddHtml($oChoice->GetName(), false)->AddHtml('</option>');
  89. }
  90. unset($oChoicesSet);
  91. $oOutput->AddHtml('</select>');
  92. $oOutput->AddHtml('</div>');
  93. // - Closing col for autocomplete & opening col for hierarchy, rendering hierarchy button, closing col and row
  94. $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">');
  95. $this->RenderHierarchicalSearch($oOutput);
  96. $oOutput->AddHtml('</div>');
  97. // - Closing row
  98. $oOutput->AddHtml('</div>');
  99. // JS FieldChange trigger (:input are not always at the same depth)
  100. $oOutput->AddJs(
  101. <<<EOF
  102. $("#{$this->oField->GetGlobalId()}").off("change keyup").on("change keyup", function(){
  103. var me = this;
  104. $(this).closest(".field_set").trigger("field_change", {
  105. id: $(me).attr("id"),
  106. name: $(me).closest(".form_field").attr("data-field-id"),
  107. value: $(me).val()
  108. });
  109. });
  110. EOF
  111. );
  112. // Attaching JS widget
  113. $oOutput->AddJs(
  114. <<<EOF
  115. $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").portal_form_field({
  116. 'validators': {$this->GetValidatorsAsJson()}
  117. });
  118. EOF
  119. );
  120. }
  121. // - For autocomplete
  122. else
  123. {
  124. $sAutocompleteFieldId = 's_ac_' . $this->oField->GetGlobalId();
  125. $sEndpoint = str_replace('-sMode-', 'autocomplete', $this->oField->GetSearchEndpoint());
  126. $sNoResultText = Dict::S('Portal:Autocomplete:NoResult');
  127. // Retrieving field value
  128. if ($this->oField->GetCurrentValue() !== null && $this->oField->GetCurrentValue() !== 0)
  129. {
  130. try
  131. {
  132. $oFieldValue = MetaModel::GetObject($sFieldValueClass, $this->oField->GetCurrentValue());
  133. }
  134. catch (CoreException $e)
  135. {
  136. IssueLog::Error('Could not retrieve object ' . $sFieldValueClass . '::' . $this->oField->GetCurrentValue() . ' for "' . $this->oField->GetId() . '" field.');
  137. throw new Exception($e->getMessage(), $e->getCode(), $e->getPrevious());
  138. }
  139. $sFieldValue = $oFieldValue->GetName();
  140. }
  141. else
  142. {
  143. $sFieldValue = '';
  144. }
  145. // HTML for autocomplete part
  146. // - Opening row
  147. $oOutput->AddHtml('<div class="row">');
  148. // - Rendering autocomplete search
  149. $oOutput->AddHtml('<div class="col-xs-' . ( $this->oField->GetHierarchical() ? 9 : 10 ) . ' col-sm-' . ( $this->oField->GetHierarchical() ? 8 : 9 ) . ' col-md-' . ( $this->oField->GetHierarchical() ? 8 : 10 ) . ' col-lg-' . ( $this->oField->GetHierarchical() ? 9 : 10 ) . '">');
  150. $oOutput->AddHtml('<input type="text" id="' . $sAutocompleteFieldId . '" name="' . $sAutocompleteFieldId . '" value="')->AddHtml($sFieldValue)->AddHtml('" data-target-id="' . $this->oField->GetGlobalId() . ' "class="form-control" />');
  151. $oOutput->AddHtml('<input type="hidden" id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" value="' . $this->oField->GetCurrentValue() . '" />');
  152. $oOutput->AddHtml('</div>');
  153. // - Rendering buttons
  154. $oOutput->AddHtml('<div class="col-xs-' . ( $this->oField->GetHierarchical() ? 3 : 2 ) . ' col-sm-' . ( $this->oField->GetHierarchical() ? 4 : 3 ) . ' col-md-' . ( $this->oField->GetHierarchical() ? 4 : 2 ) . ' col-lg-' . ( $this->oField->GetHierarchical() ? 3 : 2 ) . ' text-right">');
  155. $oOutput->AddHtml('<div class="btn-group" role="group">');
  156. // - Rendering hierarchy button
  157. $this->RenderHierarchicalSearch($oOutput);
  158. // - Rendering regular search
  159. $this->RenderRegularSearch($oOutput);
  160. $oOutput->AddHtml('</div>');
  161. $oOutput->AddHtml('</div>');
  162. // - Closing row
  163. $oOutput->AddHtml('</div>');
  164. // JS FieldChange trigger (:input are not always at the same depth)
  165. // Note : Not used for that field type
  166. // Attaching JS widget
  167. $oOutput->AddJs(
  168. <<<EOF
  169. $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").portal_form_field({
  170. 'validators': {$this->GetValidatorsAsJson()},
  171. 'get_current_value_callback': function(me, oEvent, oData){
  172. var value = null;
  173. value = me.element.find('#{$this->oField->GetGlobalId()}').val();
  174. return value;
  175. },
  176. 'set_current_value_callback': function(me, oEvent, oData){
  177. var sItemId = Object.keys(oData.value)[0];
  178. var sItemName = oData.value[sItemId];
  179. // Updating autocomplete field
  180. me.element.find('#{$this->oField->GetGlobalId()}').val(sItemId);
  181. me.element.find('#{$sAutocompleteFieldId}').val(sItemName);
  182. oAutocompleteSource_{$this->oField->GetId()}.index.datums[sItemId] = {id: sItemId, name: sItemName};
  183. //console.log('callback', oData);
  184. // Triggering field change event
  185. me.element.closest(".field_set").trigger("field_change", {
  186. id: me.element.find('#{$this->oField->GetGlobalId()}').attr("id"),
  187. name: me.element.find('#{$this->oField->GetGlobalId()}').attr("name"),
  188. value: me.element.find('#{$this->oField->GetGlobalId()}').val()
  189. });
  190. }
  191. });
  192. EOF
  193. );
  194. // Preparing JS part for autocomplete
  195. $oOutput->AddJs(
  196. <<<EOF
  197. var oAutocompleteSource_{$this->oField->GetId()} = new Bloodhound({
  198. queryTokenizer: Bloodhound.tokenizers.whitespace,
  199. datumTokenizer: Bloodhound.tokenizers.whitespace,
  200. remote: {
  201. url : '{$sEndpoint}',
  202. prepare: function(query, settings){
  203. settings.type = "POST";
  204. settings.contentType = "application/json; charset=UTF-8";
  205. settings.data = {
  206. sQuery: query,
  207. formmanager_class: $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").closest('.portal_form_handler').portal_form_handler('getOptions').formmanager_class,
  208. formmanager_data: $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").closest('.portal_form_handler').portal_form_handler('getOptions').formmanager_data,
  209. current_values: $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").closest('.portal_form_handler').portal_form_handler('getCurrentValues')
  210. }
  211. return settings;
  212. },
  213. filter: function(response){
  214. var oItems = response.results.items;
  215. // Manualy adding data from remote to the index.datums so we can check data later
  216. for(var sItemKey in oItems)
  217. {
  218. oAutocompleteSource_{$this->oField->GetId()}.index.datums[oItems[sItemKey].id] = oItems[sItemKey];
  219. }
  220. return oItems;
  221. }
  222. }
  223. });
  224. $('#$sAutocompleteFieldId').typeahead({
  225. hint: true,
  226. hightlight: true,
  227. minLength: {$this->oField->GetMinAutoCompleteChars()}
  228. },{
  229. name: '{$this->oField->GetId()}',
  230. source: oAutocompleteSource_{$this->oField->GetId()},
  231. limit: 20,
  232. display: 'name',
  233. templates: {
  234. suggestion: Handlebars.compile('<div>{{name}}</div>'),
  235. pending: $("#page_overlay .content_loader").prop('outerHTML'),
  236. notFound: '<div class="no_result">{$sNoResultText}</div>'
  237. }
  238. })
  239. .off('typeahead:select').on('typeahead:select', function(oEvent, oSuggestion){
  240. $('#{$this->oField->GetGlobalId()}').val(oSuggestion.id);
  241. // Triggering set_current_value event
  242. var oValue = {};
  243. oValue[oSuggestion.id] = oSuggestion.name;
  244. $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").trigger('set_current_value', {value: oValue});
  245. })
  246. .off('typeahead:change').on('typeahead:change', function(oEvent, oSuggestion){
  247. // 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
  248. var oDatums = oAutocompleteSource_{$this->oField->GetId()}.index.datums;
  249. var bFound = false;
  250. for(var i in oDatums)
  251. {
  252. if(oDatums[i].name == oSuggestion)
  253. {
  254. bFound = true;
  255. $('#{$this->oField->GetGlobalId()}').val(oDatums[i].id);
  256. break;
  257. }
  258. }
  259. // Emptying the fields if value is incorrect
  260. if(!bFound)
  261. {
  262. $('#{$this->oField->GetGlobalId()}').val(0);
  263. $('#{$sAutocompleteFieldId}').val('');
  264. }
  265. });
  266. EOF
  267. );
  268. }
  269. }
  270. $oOutput->AddHtml('</div>');
  271. }
  272. // ... and in read-only mode (or hidden)
  273. else
  274. {
  275. // Retrieving field value
  276. if ($this->oField->GetCurrentValue() !== null && $this->oField->GetCurrentValue() !== 0 && $this->oField->GetCurrentValue() !== '')
  277. {
  278. $oFieldValue = MetaModel::GetObject($sFieldValueClass, $this->oField->GetCurrentValue());
  279. $sFieldValue = $oFieldValue->GetName();
  280. }
  281. else
  282. {
  283. $sFieldValue = Dict::S('UI:UndefinedObject');
  284. }
  285. $oOutput->AddHtml('<div class="form-group">');
  286. // Showing label / value only if read-only but not hidden
  287. if (!$this->oField->GetHidden())
  288. {
  289. if ($this->oField->GetLabel() !== '')
  290. {
  291. $oOutput->AddHtml('<label for="' . $this->oField->GetGlobalId() . '" class="control-label">')->AddHtml($this->oField->GetLabel(), true)->AddHtml('</label>');
  292. }
  293. $oOutput->AddHtml('<div class="form-control-static">' . $sFieldValue . '</div>');
  294. }
  295. $oOutput->AddHtml('<input type="hidden" id="' . $this->oField->GetGlobalId() . '" name="' . $this->oField->GetId() . '" value="' . $this->oField->GetCurrentValue() . '" class="form-control" />');
  296. $oOutput->AddHtml('</div>');
  297. // JS FieldChange trigger (:input are not always at the same depth)
  298. $oOutput->AddJs(
  299. <<<EOF
  300. $("#{$this->oField->GetGlobalId()}").off("change keyup").on("change keyup", function(){
  301. var me = this;
  302. $(this).closest(".field_set").trigger("field_change", {
  303. id: $(me).attr("id"),
  304. name: $(me).closest(".form_field").attr("data-field-id"),
  305. value: $(me).val()
  306. });
  307. });
  308. EOF
  309. );
  310. // Attaching JS widget
  311. $oOutput->AddJs(
  312. <<<EOF
  313. $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").portal_form_field({
  314. 'validators': {$this->GetValidatorsAsJson()}
  315. });
  316. EOF
  317. );
  318. }
  319. return $oOutput;
  320. }
  321. /**
  322. * Renders an hierarchical search button
  323. *
  324. * @param RenderingOutput $oOutput
  325. */
  326. protected function RenderHierarchicalSearch(RenderingOutput &$oOutput)
  327. {
  328. if ($this->oField->GetHierarchical())
  329. {
  330. $sHierarchicalButtonId = 's_hi_' . $this->oField->GetGlobalId();
  331. $sEndpoint = str_replace('-sMode-', 'hierarchy', $this->oField->GetSearchEndpoint());
  332. $oOutput->AddHtml('<button type="button" class="btn btn-default" id="' . $sHierarchicalButtonId . '"><span class="glyphicon glyphicon-ext-hierarchy"></span></button>');
  333. $oOutput->AddJs(
  334. <<<EOF
  335. $('#{$sHierarchicalButtonId}').off('click').on('click', function(){
  336. // Creating a new modal
  337. // Note : This could be better if we check for an existing modal first instead of always creating a new one
  338. var oModalElem = $('#modal-for-all').clone();
  339. oModalElem.attr('id', '').attr('data-source-element', '{$sHierarchicalButtonId}').appendTo('body');
  340. // Resizing to small modal
  341. oModalElem.find('.modal-dialog').removeClass('modal-lg').addClass('modal-sm');
  342. // Loading content
  343. oModalElem.find('.modal-content').html($('#page_overlay .overlay_content').html());
  344. oModalElem.find('.modal-content').load(
  345. '{$sEndpoint}',
  346. {
  347. sFormPath: '{$this->oField->GetFormPath()}',
  348. sFieldId: '{$this->oField->GetId()}'
  349. }
  350. );
  351. oModalElem.modal('show');
  352. });
  353. EOF
  354. );
  355. }
  356. }
  357. /**
  358. * Renders an regular search button
  359. *
  360. * @param RenderingOutput $oOutput
  361. */
  362. protected function RenderRegularSearch(RenderingOutput &$oOutput)
  363. {
  364. $sSearchButtonId = 's_rg_' . $this->oField->GetGlobalId();
  365. $sEndpoint = str_replace('-sMode-', 'from-attribute', $this->oField->GetSearchEndpoint());
  366. $oOutput->AddHtml('<button type="button" class="btn btn-default" id="' . $sSearchButtonId . '"><span class="glyphicon glyphicon-search" aria-hidden="true"></span></button>');
  367. $oOutput->AddJs(
  368. <<<EOF
  369. $('#{$sSearchButtonId}').off('click').on('click', function(){
  370. // Creating a new modal
  371. var oModalElem;
  372. if($('.modal[data-source-element="{$sSearchButtonId}"]').length === 0)
  373. {
  374. oModalElem = $('#modal-for-all').clone();
  375. oModalElem.attr('id', '').attr('data-source-element', '{$sSearchButtonId}').appendTo('body');
  376. }
  377. else
  378. {
  379. oModalElem = $('.modal[data-source-element="{$sSearchButtonId}"]').first();
  380. }
  381. // Resizing to small modal
  382. oModalElem.find('.modal-dialog').removeClass('modal-sm').addClass('modal-lg');
  383. // Loading content
  384. oModalElem.find('.modal-content').html($('#page_overlay .overlay_content').html());
  385. oModalElem.find('.modal-content').load(
  386. '{$sEndpoint}',
  387. {
  388. sFormPath: '{$this->oField->GetFormPath()}',
  389. sFieldId: '{$this->oField->GetId()}',
  390. formmanager_class: $(this).closest('.portal_form_handler').portal_form_handler('getOptions').formmanager_class,
  391. formmanager_data: JSON.stringify($(this).closest('.portal_form_handler').portal_form_handler('getOptions').formmanager_data),
  392. current_values: $(this).closest('.portal_form_handler').portal_form_handler('getCurrentValues')
  393. }
  394. );
  395. oModalElem.modal('show');
  396. });
  397. EOF
  398. );
  399. }
  400. }