bsselectobjectfieldrenderer.class.inc.php 15 KB

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