property_field.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742
  1. //iTop Designer widget for editing properties line by line
  2. $(function()
  3. {
  4. // the widget definition, where "itop" is the namespace,
  5. // "property_field" the widget name
  6. $.widget( "itop.property_field",
  7. {
  8. // default options
  9. options:
  10. {
  11. parent_selector: null,
  12. field_id: '',
  13. get_field_value: null,
  14. equals: null,
  15. submit_to: 'index.php',
  16. submit_parameters: {operation: 'async_action'},
  17. do_apply: null,
  18. do_cancel: null,
  19. auto_apply: false,
  20. can_apply: true
  21. },
  22. // the constructor
  23. _create: function()
  24. {
  25. var me = this;
  26. this.element
  27. .addClass( "itop-property-field" )
  28. .bind('apply_changes.itop-property-field', function(){me._do_apply();} );
  29. this.bModified = false;
  30. if (this.options.field_id != '')
  31. {
  32. // In case there is an hidden input having the same id (somewhere else in the page), the change event does not occur unless the input loses the focus
  33. // To reduce the impact, let's handle keyup as well
  34. $('#'+this.options.field_id, this.element).bind('change.itop-property-field keyup.itop-property-field', function() { me._on_change(); });
  35. this.value = this._get_field_value();
  36. }
  37. this.element.find(".prop_apply").bind('click.itop-property-field', function() { me._do_apply(); });
  38. this.element.find(".prop_cancel").bind('click.itop-property-field', function() { me._do_cancel(); });
  39. this._refresh();
  40. },
  41. // called when created, and later when changing options
  42. _refresh: function()
  43. {
  44. if (this.bModified)
  45. {
  46. this.element.addClass("itop-property-field-modified");
  47. if (this.options.can_apply)
  48. {
  49. this.element.find(".prop_icon span.ui-icon-circle-check").css({visibility: ''});
  50. }
  51. else
  52. {
  53. this.element.find(".prop_icon span.ui-icon-circle-check").css({visibility: 'hidden'});
  54. }
  55. this.element.find(".prop_icon span.ui-icon-circle-close").css({visibility: ''});
  56. }
  57. else
  58. {
  59. this.element.removeClass("itop-property-field-modified");
  60. this.element.find(".prop_icon span.ui-icon-circle-check").css({visibility: 'hidden'});
  61. this.element.find(".prop_icon span.ui-icon-circle-close").css({visibility: 'hidden'});
  62. }
  63. },
  64. // events bound via _bind are removed automatically
  65. // revert other modifications here
  66. _destroy: function()
  67. {
  68. this.element.removeClass( "itop-property-field" );
  69. this.element.removeClass("itop-property-field-modified");
  70. },
  71. // _setOptions is called with a hash of all options that are changing
  72. // always refresh when changing options
  73. _setOptions: function()
  74. {
  75. // in 1.9 would use _superApply
  76. this._superApply(arguments);
  77. this._refresh();
  78. },
  79. // _setOption is called for each individual option that is changing
  80. _setOption: function( key, value )
  81. {
  82. // in 1.9 would use _super
  83. this._superApply(arguments);
  84. },
  85. _on_change: function()
  86. {
  87. var new_value = this._get_field_value();
  88. if (this._equals(new_value, this.value))
  89. {
  90. this.bModified = false;
  91. this._notify_as_clean(true);
  92. }
  93. else
  94. {
  95. this.bModified = true;
  96. if (this.options.auto_apply && this.options.can_apply)
  97. {
  98. this._do_apply();
  99. }
  100. else
  101. {
  102. this._notify_as_dirty();
  103. }
  104. }
  105. this._refresh();
  106. if (this.options.parent_selector)
  107. {
  108. $(this.options.parent_selector).trigger('subitem_changed');
  109. }
  110. },
  111. _notify_as_dirty: function()
  112. {
  113. this.element.closest('form').trigger('property_field_dirty', { id: this.options.field_id, reason: 'modified' });
  114. },
  115. _notify_as_clean: function(bRevert)
  116. {
  117. this.element.closest('form').trigger('property_field_clean', { id: this.options.field_id, reason: (bRevert ? 'revert' : 'apply') });
  118. },
  119. _equals: function( value1, value2 )
  120. {
  121. if (this.options.equals === null)
  122. {
  123. return value1 == value2;
  124. }
  125. else
  126. {
  127. return this.options.equals(value1, value2);
  128. }
  129. },
  130. _get_field_value: function()
  131. {
  132. if (this.options.get_field_value === null)
  133. {
  134. var oField = $('#'+this.options.field_id, this.element);
  135. if (oField.attr('type') == 'checkbox')
  136. {
  137. return (oField.attr('checked') == 'checked');
  138. }
  139. else
  140. {
  141. return oField.val();
  142. }
  143. }
  144. else
  145. {
  146. return this.options.get_field_value();
  147. }
  148. },
  149. get_field_value: function()
  150. {
  151. return this._get_field_value();
  152. },
  153. get_field_name: function()
  154. {
  155. return $('#'+this.options.field_id, this.element).attr('name');
  156. },
  157. _get_committed_value: function()
  158. {
  159. return { name: $('#'+this.options.field_id, this.element).attr('name'), value: this.value };
  160. },
  161. _get_current_value: function()
  162. {
  163. return { name: $('#'+this.options.field_id, this.element).attr('name'), value: this._get_field_value() };
  164. },
  165. _do_apply: function()
  166. {
  167. if (this.options.parent_selector)
  168. {
  169. $(this.options.parent_selector).trigger('mark_as_modified');
  170. }
  171. if (this.options.do_apply)
  172. {
  173. // specific behavior...
  174. if (this.options.do_apply())
  175. {
  176. this.bModified = false;
  177. this.previous_value = this.value;
  178. this.value = this._get_field_value();
  179. this._refresh();
  180. }
  181. }
  182. else
  183. {
  184. // Validate the field
  185. sFormId = this.element.closest('form').attr('id');
  186. var oField = $('#'+this.options.field_id, this.element);
  187. oField.trigger('validate');
  188. if ( $.inArray(this.options.field_id, oFormValidation[sFormId]) == -1)
  189. {
  190. this.bModified = false;
  191. this.previous_value = this.value;
  192. this.value = this._get_field_value();
  193. this._do_submit();
  194. this._refresh();
  195. }
  196. }
  197. this._notify_as_clean(false);
  198. },
  199. _do_cancel: function()
  200. {
  201. if (this.options.do_cancel)
  202. {
  203. // specific behavior...
  204. this.options.do_cancel();
  205. }
  206. else
  207. {
  208. this.bModified = false;
  209. var oField = $('#'+this.options.field_id, this.element);
  210. if (oField.attr('type') == 'checkbox')
  211. {
  212. if (this.value)
  213. {
  214. oField.attr('checked', true);
  215. }
  216. else
  217. {
  218. oField.removeAttr('checked');
  219. }
  220. }
  221. else
  222. {
  223. oField.val(this.value);
  224. }
  225. this._refresh();
  226. oField.trigger('reverted', {type: 'designer', previous_value: this.value });
  227. oField.trigger('validate');
  228. if (this.options.parent_selector)
  229. {
  230. $(this.options.parent_selector).trigger('subitem_changed');
  231. }
  232. }
  233. this._notify_as_clean(true);
  234. },
  235. _do_submit: function()
  236. {
  237. var oData = {};
  238. var me = this;
  239. this.element.closest('form').find(':input[type=hidden]').each(function()
  240. {
  241. // Hidden form fields
  242. oData[$(this).attr('name')] = $(this).val();
  243. });
  244. this.element.closest('form').find('.itop-property-field').each(function()
  245. {
  246. var oWidget = me._get_widget($(this));
  247. if (oWidget && oWidget._is_visible())
  248. {
  249. var oVal = oWidget._get_committed_value();
  250. oData[oVal.name] = oVal.value;
  251. }
  252. });
  253. oPostedData = this.options.submit_parameters;
  254. oPostedData.params = oData;
  255. oPostedData.params.updated = [ $('#'+this.options.field_id, this.element).attr('name') ]; // only one field updated in this case
  256. oPostedData.params.previous_values = {};
  257. oPostedData.params.previous_values[oPostedData.params.updated] = this.previous_value; // pass also the previous value(s)
  258. $.post(this.options.submit_to, oPostedData, function(data)
  259. {
  260. $('#prop_submit_result').html(data);
  261. });
  262. },
  263. _is_visible: function()
  264. {
  265. return this.element.is(':visible');
  266. },
  267. mark_as_applied: function()
  268. {
  269. this.bModified = false;
  270. this.previous_value = this.value;
  271. this.value = this._get_field_value();
  272. this._refresh();
  273. },
  274. validate: function()
  275. {
  276. var oField = $('#'+this.options.field_id, this.element);
  277. oField.trigger('validate');
  278. },
  279. _get_widget: function(element)
  280. {
  281. var oWidget = element.data('itopProperty_field');
  282. if (oWidget == undefined)
  283. {
  284. oWidget = element.data('itopSelector_property_field');
  285. }
  286. return oWidget;
  287. }
  288. });
  289. });
  290. $(function()
  291. {
  292. // the widget definition, where "itop" is the namespace,
  293. // "selector_property_field" the widget name
  294. $.widget( "itop.selector_property_field", $.itop.property_field,
  295. {
  296. // default options
  297. options:
  298. {
  299. parent_selector: null,
  300. field_id: '',
  301. get_field_value: null,
  302. equals: null,
  303. submit_to: 'index.php',
  304. submit_parameters: {operation: 'async_action'},
  305. do_apply: null,
  306. do_cancel: null,
  307. auto_apply: false,
  308. can_apply: true,
  309. data_selector: ''
  310. },
  311. // the constructor
  312. _create: function()
  313. {
  314. var me = this;
  315. this._superApply();
  316. this.element
  317. .addClass( "itop-selector-property-field" );
  318. $('#'+this.options.field_id).bind('reverted init', function() {
  319. me._update_subform();
  320. }).trigger('init'); // initial refresh
  321. this.element.bind('subitem_changed', function() {
  322. me._on_subitem_changed();
  323. });
  324. },
  325. _update_subform: function()
  326. {
  327. var sSelector = this.options.data_selector;
  328. var me = this;
  329. // Mark all the direct children as hidden
  330. $('tr[data-selector="'+sSelector+'"]').attr('data-state', 'hidden');
  331. // Mark the selected one as visible
  332. var sSelectedHierarchy = sSelector+'-'+$('#'+this.options.field_id).val();
  333. $('tr[data-path="'+sSelectedHierarchy+'"]').attr('data-state', 'visible');
  334. // Show all items behind the current one
  335. $('tr[data-path^="'+sSelector+'"]').show();
  336. // Hide items behind the current one as soon as it is behind a hidden node (or itself is marked as hidden)
  337. $('tr[data-path^="'+sSelector+'"][data-state="hidden"]').each(function() {
  338. $(this).hide();
  339. var sPath = $(this).attr('data-path');
  340. $('tr[data-path^="'+sPath+'/"]').hide();
  341. });
  342. $('tr[data-path^="'+sSelector+'"]').each(function() {
  343. if($(this).is(':visible'))
  344. {
  345. var oWidget = me._get_widget($(this).closest('.itop-property-field'));
  346. if (oWidget)
  347. {
  348. try
  349. {
  350. oWidget._setOptions({can_apply: !me.bModified, parent_selector: '#'+me.element.attr('id') });
  351. oWidget.validate();
  352. }
  353. catch(e)
  354. {
  355. // Do nothing, form in read-only mode
  356. }
  357. }
  358. }
  359. });
  360. },
  361. // events bound via _bind are removed automatically
  362. // revert other modifications here
  363. _destroy: function()
  364. {
  365. this.element.removeClass( "itop-selector-property-field" );
  366. this._superApply();
  367. },
  368. _on_change: function()
  369. {
  370. var new_value = this._get_field_value();
  371. if (this._equals(new_value, this.value))
  372. {
  373. this.bModified = false;
  374. }
  375. else
  376. {
  377. this.bModified = true;
  378. }
  379. this._update_subform();
  380. if (this.options.auto_apply && this.options.can_apply)
  381. {
  382. this._do_apply();
  383. }
  384. this._on_subitem_changed(); // initial validation
  385. this._refresh();
  386. },
  387. _do_apply: function()
  388. {
  389. this._superApply();
  390. this._update_subform();
  391. },
  392. _do_submit: function()
  393. {
  394. var oData = {};
  395. this.element.closest('form').find(':input[type=hidden]').each(function()
  396. {
  397. // Hidden form fields
  398. oData[$(this).attr('name')] = $(this).val();
  399. });
  400. var sSelector = this.options.data_selector;
  401. var me = this;
  402. var aUpdated = [];
  403. $('tr[data-path^="'+sSelector+'"]').each(function() {
  404. if($(this).is(':visible'))
  405. {
  406. var oWidget = me._get_widget($(this).closest('.itop-property-field'));
  407. if (oWidget)
  408. {
  409. oWidget.mark_as_applied();
  410. sName = oWidget.get_field_name();
  411. if (typeof sName == 'string')
  412. {
  413. aUpdated.push(sName);
  414. }
  415. }
  416. }
  417. });
  418. this.element.closest('form').find('.itop-property-field').each(function()
  419. {
  420. var oWidget = me._get_widget($(this));
  421. if (oWidget && oWidget._is_visible())
  422. {
  423. var oVal = oWidget._get_committed_value();
  424. oData[oVal.name] = oVal.value;
  425. }
  426. });
  427. var oPostedData = this.options.submit_parameters;
  428. var sName = $('#'+this.options.field_id, this.element).attr('name');
  429. oPostedData.params = oData;
  430. oPostedData.params.updated = [];
  431. aUpdated.push(sName); // several fields updated in this case
  432. oPostedData.params.updated = aUpdated;
  433. oPostedData.params.previous_values = {};
  434. oPostedData.params.previous_values[sName] = this.previous_value; // pass also the previous value(s)
  435. $.post(this.options.submit_to, oPostedData, function(data)
  436. {
  437. $('#prop_submit_result').html(data);
  438. });
  439. },
  440. _on_subitem_changed : function()
  441. {
  442. sFormId = this.element.closest('form').attr('id');
  443. oFormValidation[sFormId] = [];
  444. this.options.can_apply = true;
  445. var sSelector = this.options.data_selector;
  446. var me = this;
  447. $('tr[data-path^="'+sSelector+'"]').each(function() {
  448. if($(this).is(':visible'))
  449. {
  450. var oWidget = me._get_widget($(this).closest('.itop-property-field'));
  451. if (oWidget)
  452. {
  453. oWidget.validate();
  454. }
  455. }
  456. });
  457. this.options.can_apply = (oFormValidation[sFormId].length == 0); // apply allowed only if no error
  458. this._refresh();
  459. }
  460. });
  461. });
  462. var oFormValidation = {};
  463. function ValidateWithPattern(sFieldId, bMandatory, sPattern, sFormId, aForbiddenValues)
  464. {
  465. var currentVal = $('#'+sFieldId).val();
  466. var bValid = true;
  467. var sMessage = null;
  468. if (bMandatory && (currentVal == ''))
  469. {
  470. bValid = false;
  471. }
  472. if ((sPattern != '') && (currentVal != ''))
  473. {
  474. re = new RegExp(sPattern);
  475. bValid = re.test(currentVal);
  476. }
  477. if (aForbiddenValues)
  478. {
  479. for(var i in aForbiddenValues)
  480. {
  481. for(j in aForbiddenValues[i].values)
  482. {
  483. if (aForbiddenValues[i].case_sensitive)
  484. {
  485. if (aForbiddenValues[i].values[j] == currentVal)
  486. {
  487. bValid = false;
  488. sMessage = aForbiddenValues[i].message;
  489. break;
  490. }
  491. }
  492. else
  493. {
  494. if (aForbiddenValues[i].values[j].toUpperCase() == currentVal.toUpperCase())
  495. {
  496. bValid = false;
  497. sMessage = aForbiddenValues[i].message;
  498. break;
  499. }
  500. }
  501. }
  502. }
  503. }
  504. if (oFormValidation[sFormId] == undefined) oFormValidation[sFormId] = [];
  505. if (!bValid)
  506. {
  507. $('#v_'+sFieldId).addClass('ui-state-error');
  508. iFieldIdPos = jQuery.inArray(sFieldId, oFormValidation[sFormId]);
  509. if (iFieldIdPos == -1)
  510. {
  511. oFormValidation[sFormId].push(sFieldId);
  512. }
  513. if (sMessage)
  514. {
  515. $('#'+sFieldId).attr('title', sMessage).tooltip();
  516. if ($('#'+sFieldId).is(":focus"))
  517. {
  518. $('#'+sFieldId).tooltip('open');
  519. }
  520. }
  521. }
  522. else
  523. {
  524. $('#v_'+sFieldId).removeClass('ui-state-error');
  525. if ($('#'+sFieldId).data('uiTooltip'))
  526. {
  527. $('#'+sFieldId).tooltip('close');
  528. }
  529. $('#'+sFieldId).removeAttr('title');
  530. // Remove the element from the array
  531. iFieldIdPos = jQuery.inArray(sFieldId, oFormValidation[sFormId]);
  532. if (iFieldIdPos > -1)
  533. {
  534. oFormValidation[sFormId].splice(iFieldIdPos, 1);
  535. }
  536. }
  537. }
  538. function ValidateInteger(sFieldId, bMandatory, sFormId, iMin, iMax, sExplainFormat)
  539. {
  540. var currentVal = $('#'+sFieldId).val();
  541. var bValid = true;
  542. var sMessage = null;
  543. if (bMandatory && (currentVal == ''))
  544. {
  545. bValid = false;
  546. }
  547. re = new RegExp('^$|^-?[0-9]+$');
  548. bValid = re.test(currentVal);
  549. if (bValid && (currentVal != ''))
  550. {
  551. // It is a valid number, let's check the boundaries
  552. var iValue = parseInt(currentVal, 10);
  553. if ((iMin != null) && (iValue < iMin))
  554. {
  555. bValid = false;
  556. }
  557. if ((iMax != null) && (iValue > iMax))
  558. {
  559. bValid = false;
  560. }
  561. if (!bValid && (sExplainFormat != undefined))
  562. {
  563. sMessage = sExplainFormat;
  564. }
  565. }
  566. if (oFormValidation[sFormId] == undefined) oFormValidation[sFormId] = [];
  567. if (!bValid)
  568. {
  569. $('#v_'+sFieldId).addClass('ui-state-error');
  570. iFieldIdPos = jQuery.inArray(sFieldId, oFormValidation[sFormId]);
  571. if (iFieldIdPos == -1)
  572. {
  573. oFormValidation[sFormId].push(sFieldId);
  574. }
  575. if (sMessage)
  576. {
  577. $('#'+sFieldId).attr('title', sMessage).tooltip();
  578. if ($('#'+sFieldId).is(":focus"))
  579. {
  580. $('#'+sFieldId).tooltip('open');
  581. }
  582. }
  583. }
  584. else
  585. {
  586. $('#v_'+sFieldId).removeClass('ui-state-error');
  587. if ($('#'+sFieldId).data('uiTooltip'))
  588. {
  589. $('#'+sFieldId).tooltip('close');
  590. }
  591. $('#'+sFieldId).removeAttr('title');
  592. // Remove the element from the array
  593. iFieldIdPos = jQuery.inArray(sFieldId, oFormValidation[sFormId]);
  594. if (iFieldIdPos > -1)
  595. {
  596. oFormValidation[sFormId].splice(iFieldIdPos, 1);
  597. }
  598. }
  599. }
  600. function ValidateForm(sFormId, bValidateAll)
  601. {
  602. oFormValidation[sFormId] = [];
  603. if (bValidateAll)
  604. {
  605. $('#'+sFormId+' :input').trigger('validate');
  606. }
  607. else
  608. {
  609. // Only the visible fields
  610. $('#'+sFormId+' :input:visible').each(function() {
  611. $(this).trigger('validate');
  612. });
  613. }
  614. return oFormValidation[sFormId];
  615. }
  616. function ReadFormParams(sFormId)
  617. {
  618. var oMap = { };
  619. $('#'+sFormId+' :input').each( function() {
  620. if ($(this).parent().is(':visible') && !$(this).prop('disabled'))
  621. {
  622. var sName = $(this).attr('name');
  623. if (sName && sName != '')
  624. {
  625. if (this.type == 'checkbox')
  626. {
  627. oMap[sName] = ($(this).attr('checked') == 'checked');
  628. }
  629. else if (this.type == 'radio')
  630. {
  631. if ($(this).prop('checked'))
  632. {
  633. oMap[sName] = $(this).val();
  634. }
  635. }
  636. else
  637. {
  638. oMap[sName] = $(this).val();
  639. }
  640. }
  641. }
  642. });
  643. return oMap;
  644. }
  645. function SubmitForm(sFormId, onSubmitResult)
  646. {
  647. var aErrors = ValidateForm(sFormId, false);
  648. if (aErrors.length == 0)
  649. {
  650. var oMap = ReadFormParams(sFormId);
  651. oMap.module_name = sCurrentModule;
  652. $('#'+sFormId+' :input').each( function() {
  653. if ($(this).parent().is(':visible'))
  654. {
  655. var sName = $(this).attr('name');
  656. if (sName && sName != '')
  657. {
  658. if (this.type == 'checkbox')
  659. {
  660. oMap[sName] = ($(this).attr('checked') == 'checked');
  661. }
  662. else
  663. {
  664. oMap[sName] = $(this).val();
  665. }
  666. }
  667. }
  668. });
  669. $.post(GetAbsoluteUrlAppRoot()+'designer/module.php', oMap, function(data)
  670. {
  671. onSubmitResult(data);
  672. });
  673. }
  674. else
  675. {
  676. // TODO: better error reporting !!!
  677. alert('Please fill all the fields before continuing...');
  678. }
  679. }
  680. function RemoveSubForm(sId, sUrl, oParams)
  681. {
  682. $.post(sUrl, oParams, function(data) {
  683. $('body').append(data);
  684. });
  685. }
  686. function AddSubForm(sId, sUrl, oParams)
  687. {
  688. var aIndexes = JSON.parse($('#'+sId).val());
  689. var iLast = aIndexes[aIndexes.length - 1];
  690. var iNewIdx = 1 + iLast;
  691. oParams.new_index = iNewIdx;
  692. $.post(sUrl, oParams, function(data) {
  693. $('body').append(data);
  694. });
  695. }