property_field.js 17 KB

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