Admin.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. /*
  2. This file is part of the Sonata package.
  3. (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
  4. For the full copyright and license information, please view the LICENSE
  5. file that was distributed with this source code.
  6. */
  7. jQuery(document).ready(function() {
  8. jQuery('html').removeClass('no-js');
  9. if (window.SONATA_CONFIG && window.SONATA_CONFIG.CONFIRM_EXIT) {
  10. jQuery('.sonata-ba-form form').each(function () { jQuery(this).confirmExit(); });
  11. }
  12. Admin.setup_per_page_switcher(document);
  13. Admin.setup_collection_buttons(document);
  14. Admin.shared_setup(document);
  15. });
  16. jQuery(document).on('sonata-admin-append-form-element', function(e) {
  17. Admin.setup_select2(e.target);
  18. Admin.setup_icheck(e.target);
  19. Admin.setup_collection_counter(e.target);
  20. });
  21. var Admin = {
  22. collectionCounters: [],
  23. /**
  24. * This function must called when a ajax call is done, to ensure
  25. * retrieve html is properly setup
  26. *
  27. * @param subject
  28. */
  29. shared_setup: function(subject) {
  30. Admin.log("[core|shared_setup] Register services on", subject);
  31. Admin.set_object_field_value(subject);
  32. Admin.setup_select2(subject);
  33. Admin.setup_icheck(subject);
  34. Admin.add_filters(subject);
  35. Admin.setup_xeditable(subject);
  36. Admin.add_pretty_errors(subject);
  37. Admin.setup_form_tabs_for_errors(subject);
  38. Admin.setup_inline_form_errors(subject);
  39. Admin.setup_tree_view(subject);
  40. Admin.setup_collection_counter(subject);
  41. Admin.setup_sticky_elements(subject);
  42. // Admin.setup_list_modal(subject);
  43. },
  44. setup_list_modal: function(modal) {
  45. Admin.log('[core|setup_list_modal] configure modal on', modal);
  46. // this will force relation modal to open list of entity in a wider modal
  47. // to improve readability
  48. jQuery('div.modal-dialog', modal).css({
  49. width: '90%', //choose your width
  50. height: '85%',
  51. padding: 0
  52. });
  53. jQuery('div.modal-content', modal).css({
  54. 'border-radius':'0',
  55. height: '100%',
  56. padding: 0
  57. });
  58. jQuery('.modal-body', modal).css({
  59. width: 'auto',
  60. height: '90%',
  61. padding: 5,
  62. overflow: 'scroll'
  63. });
  64. },
  65. setup_select2: function(subject) {
  66. if (window.SONATA_CONFIG && window.SONATA_CONFIG.USE_SELECT2 && window.Select2) {
  67. Admin.log('[core|setup_select2] configure Select2 on', subject);
  68. jQuery('select:not([data-sonata-select2="false"])', subject).each(function() {
  69. var select = jQuery(this);
  70. var allowClearEnabled = false;
  71. var popover = select.data('popover');
  72. select.removeClass('form-control');
  73. if (select.find('option[value=""]').length || select.attr('data-sonata-select2-allow-clear')==='true') {
  74. allowClearEnabled = true;
  75. } else if (select.attr('data-sonata-select2-allow-clear')==='false') {
  76. allowClearEnabled = false;
  77. }
  78. select.select2({
  79. width: function(){
  80. return Admin.get_select2_width(this.element);
  81. },
  82. dropdownAutoWidth: true,
  83. minimumResultsForSearch: 10,
  84. allowClear: allowClearEnabled
  85. });
  86. if (undefined !== popover) {
  87. select
  88. .select2('container')
  89. .popover(popover.options)
  90. ;
  91. }
  92. });
  93. }
  94. },
  95. setup_icheck: function(subject) {
  96. if (window.SONATA_CONFIG && window.SONATA_CONFIG.USE_ICHECK) {
  97. Admin.log('[core|setup_icheck] configure iCheck on', subject);
  98. jQuery("input[type='checkbox']:not('label.btn>input'), input[type='radio']:not('label.btn>input')", subject).iCheck({
  99. checkboxClass: 'icheckbox_square-blue',
  100. radioClass: 'iradio_sqaure-blue'
  101. });
  102. }
  103. },
  104. setup_xeditable: function(subject) {
  105. Admin.log('[core|setup_xeditable] configure xeditable on', subject);
  106. jQuery('.x-editable', subject).editable({
  107. emptyclass: 'editable-empty btn btn-sm btn-default',
  108. emptytext: '<i class="glyphicon glyphicon-edit"></i>',
  109. container: 'body',
  110. placement: 'auto',
  111. success: function(response) {
  112. if('KO' === response.status) {
  113. return response.message;
  114. }
  115. var html = jQuery(response.content);
  116. Admin.setup_xeditable(html);
  117. jQuery(this)
  118. .closest('td')
  119. .replaceWith(html)
  120. ;
  121. }
  122. });
  123. },
  124. /**
  125. * render log message
  126. * @param mixed
  127. */
  128. log: function() {
  129. var msg = '[Sonata.Admin] ' + Array.prototype.join.call(arguments,', ');
  130. if (window.console && window.console.log) {
  131. window.console.log(msg);
  132. } else if (window.opera && window.opera.postError) {
  133. window.opera.postError(msg);
  134. }
  135. },
  136. /**
  137. * display related errors messages
  138. *
  139. * @param subject
  140. */
  141. add_pretty_errors: function(subject) {
  142. Admin.log('[core|add_pretty_errors] configure pretty errors on', subject);
  143. jQuery('div.sonata-ba-field-error', subject).each(function(index, element) {
  144. var input = jQuery(':input', element);
  145. if (!input.length) {
  146. return;
  147. }
  148. var message = jQuery('div.sonata-ba-field-error-messages', element).html();
  149. jQuery('div.sonata-ba-field-error-messages', element).remove();
  150. if (!message || message.length == 0) {
  151. return;
  152. }
  153. var target = input,
  154. fieldShortDescription = input.closest('.field-container').find('.field-short-description'),
  155. select2 = input.closest('.select2-container')
  156. ;
  157. if (fieldShortDescription.length) {
  158. target = fieldShortDescription;
  159. } else if (select2.length) {
  160. target= select2;
  161. }
  162. target.popover({
  163. content: message,
  164. trigger: 'hover',
  165. html: true,
  166. placement: 'top',
  167. template: '<div class="popover"><div class="arrow"></div><div class="popover-inner"><div class="popover-content alert-error"><p></p></div></div></div>'
  168. });
  169. });
  170. },
  171. stopEvent: function(event) {
  172. // https://github.com/sonata-project/SonataAdminBundle/issues/151
  173. //if it is a standard browser use preventDefault otherwise it is IE then return false
  174. if(event.preventDefault) {
  175. event.preventDefault();
  176. } else {
  177. event.returnValue = false;
  178. }
  179. //if it is a standard browser get target otherwise it is IE then adapt syntax and get target
  180. if (typeof event.target != 'undefined') {
  181. targetElement = event.target;
  182. } else {
  183. targetElement = event.srcElement;
  184. }
  185. return targetElement;
  186. },
  187. add_filters: function(subject) {
  188. Admin.log('[core|add_filters] configure filters on', subject);
  189. jQuery('a.sonata-toggle-filter', subject).on('click', function(e) {
  190. e.preventDefault();
  191. e.stopPropagation();
  192. if (jQuery(e.target).attr('sonata-filter') == 'false') {
  193. return;
  194. }
  195. Admin.log('[core|add_filters] handle filter container: ', jQuery(e.target).attr('filter-container'))
  196. var filters_container = jQuery('#' + jQuery(e.currentTarget).attr('filter-container'));
  197. if (jQuery('div[sonata-filter="true"]:visible', filters_container).length == 0) {
  198. jQuery(filters_container).slideDown();
  199. }
  200. var targetSelector = jQuery(e.currentTarget).attr('filter-target'),
  201. target = jQuery('div[id="' + targetSelector + '"]', filters_container),
  202. filterToggler = jQuery('i', '.sonata-toggle-filter[filter-target="' + targetSelector + '"]')
  203. ;
  204. if (jQuery(target).is(":visible")) {
  205. filterToggler
  206. .removeClass('fa-check-square-o')
  207. .addClass('fa-square-o')
  208. ;
  209. target.hide();
  210. } else {
  211. filterToggler
  212. .removeClass('fa-square-o')
  213. .addClass('fa-check-square-o')
  214. ;
  215. target.show();
  216. }
  217. if (jQuery('div[sonata-filter="true"]:visible', filters_container).length > 0) {
  218. jQuery(filters_container).slideDown();
  219. } else {
  220. jQuery(filters_container).slideUp();
  221. }
  222. });
  223. jQuery('.sonata-filter-form', subject).on('submit', function () {
  224. jQuery(this).find('[sonata-filter="true"]:hidden :input').val('');
  225. });
  226. /* Advanced filters */
  227. if (jQuery('.advanced-filter :input:visible', subject).filter(function () { return jQuery(this).val() }).length === 0) {
  228. jQuery('.advanced-filter').hide();
  229. };
  230. jQuery('[data-toggle="advanced-filter"]', subject).click(function() {
  231. jQuery('.advanced-filter').toggle();
  232. });
  233. },
  234. /**
  235. * Change object field value
  236. * @param subject
  237. */
  238. set_object_field_value: function(subject) {
  239. Admin.log('[core|set_object_field_value] set value field on', subject);
  240. this.log(jQuery('a.sonata-ba-edit-inline', subject));
  241. jQuery('a.sonata-ba-edit-inline', subject).click(function(event) {
  242. Admin.stopEvent(event);
  243. var subject = jQuery(this);
  244. jQuery.ajax({
  245. url: subject.attr('href'),
  246. type: 'POST',
  247. success: function(json) {
  248. if(json.status === "OK") {
  249. var elm = jQuery(subject).parent();
  250. elm.children().remove();
  251. // fix issue with html comment ...
  252. elm.html(jQuery(json.content.replace(/<!--[\s\S]*?-->/g, "")).html());
  253. elm.effect("highlight", {'color' : '#57A957'}, 2000);
  254. Admin.set_object_field_value(elm);
  255. } else {
  256. jQuery(subject).parent().effect("highlight", {'color' : '#C43C35'}, 2000);
  257. }
  258. }
  259. });
  260. });
  261. },
  262. setup_collection_counter: function(subject) {
  263. Admin.log('[core|setup_collection_counter] setup collection counter', subject);
  264. // Count and save element of each collection
  265. var highestCounterRegexp = new RegExp('_([0-9])+$');
  266. jQuery(subject).find('[data-prototype]').each(function() {
  267. var collection = jQuery(this);
  268. var counter = 0;
  269. collection.children().each(function() {
  270. var matches = highestCounterRegexp.exec(jQuery('[id^="sonata-ba-field-container"]', this).attr('id'));
  271. if (matches && matches[1] && matches[1] > counter) {
  272. counter = parseInt(matches[1], 10);
  273. }
  274. });
  275. Admin.collectionCounters[collection.attr('id')] = counter;
  276. });
  277. },
  278. setup_collection_buttons: function(subject) {
  279. jQuery(subject).on('click', '.sonata-collection-add', function(event) {
  280. Admin.stopEvent(event);
  281. var container = jQuery(this).closest('[data-prototype]');
  282. var counter = ++Admin.collectionCounters[container.attr('id')];
  283. var proto = container.attr('data-prototype');
  284. var protoName = container.attr('data-prototype-name') || '__name__';
  285. // Set field id
  286. var idRegexp = new RegExp(container.attr('id')+'_'+protoName,'g');
  287. proto = proto.replace(idRegexp, container.attr('id')+'_'+counter);
  288. // Set field name
  289. var parts = container.attr('id').split('_');
  290. var nameRegexp = new RegExp(parts[parts.length-1]+'\\]\\['+protoName,'g');
  291. proto = proto.replace(nameRegexp, parts[parts.length-1]+']['+counter);
  292. jQuery(proto)
  293. .insertBefore(jQuery(this).parent())
  294. .trigger('sonata-admin-append-form-element')
  295. ;
  296. jQuery(this).trigger('sonata-collection-item-added');
  297. });
  298. jQuery(subject).on('click', '.sonata-collection-delete', function(event) {
  299. Admin.stopEvent(event);
  300. jQuery(this).trigger('sonata-collection-item-deleted');
  301. jQuery(this).closest('.sonata-collection-row').remove();
  302. });
  303. },
  304. setup_per_page_switcher: function(subject) {
  305. Admin.log('[core|setup_per_page_switcher] setup page switcher', subject);
  306. jQuery('select.per-page').change(function(event) {
  307. jQuery('input[type=submit]').hide();
  308. window.top.location.href=this.options[this.selectedIndex].value;
  309. });
  310. },
  311. setup_form_tabs_for_errors: function(subject) {
  312. Admin.log('[core|setup_form_tabs_for_errors] setup form tab\'s errors', subject);
  313. // Switch to first tab with server side validation errors on page load
  314. jQuery('form', subject).each(function() {
  315. Admin.show_form_first_tab_with_errors(jQuery(this), '.sonata-ba-field-error');
  316. });
  317. // Switch to first tab with HTML5 errors on form submit
  318. jQuery(subject)
  319. .on('click', 'form [type="submit"]', function() {
  320. Admin.show_form_first_tab_with_errors(jQuery(this).closest('form'), ':invalid');
  321. })
  322. .on('keypress', 'form [type="text"]', function(e) {
  323. if (13 === e.which) {
  324. Admin.show_form_first_tab_with_errors(jQuery(this), ':invalid');
  325. }
  326. })
  327. ;
  328. },
  329. show_form_first_tab_with_errors: function(form, errorSelector) {
  330. Admin.log('[core|show_form_first_tab_with_errors] show first tab with errors', form);
  331. var tabs = form.find('.nav-tabs a'), firstTabWithErrors;
  332. tabs.each(function() {
  333. var id = jQuery(this).attr('href'),
  334. tab = jQuery(this),
  335. icon = tab.find('.has-errors');
  336. if (jQuery(id).find(errorSelector).length > 0) {
  337. // Only show first tab with errors
  338. if (!firstTabWithErrors) {
  339. tab.tab('show');
  340. firstTabWithErrors = tab;
  341. }
  342. icon.removeClass('hide');
  343. } else {
  344. icon.addClass('hide');
  345. }
  346. });
  347. },
  348. setup_inline_form_errors: function(subject) {
  349. Admin.log('[core|setup_inline_form_errors] show first tab with errors', subject);
  350. var deleteCheckboxSelector = '.sonata-ba-field-inline-table [id$="_delete"][type="checkbox"]';
  351. jQuery(deleteCheckboxSelector, subject).each(function() {
  352. Admin.switch_inline_form_errors(jQuery(this));
  353. });
  354. jQuery(subject).on('change', deleteCheckboxSelector, function() {
  355. Admin.switch_inline_form_errors(jQuery(this));
  356. });
  357. },
  358. /**
  359. * Disable inline form errors when the row is marked for deletion
  360. */
  361. switch_inline_form_errors: function(subject) {
  362. Admin.log('[core|switch_inline_form_errors] switch_inline_form_errors', subject);
  363. var row = subject.closest('.sonata-ba-field-inline-table'),
  364. errors = row.find('.sonata-ba-field-error-messages')
  365. ;
  366. if (subject.is(':checked')) {
  367. row
  368. .find('[required]')
  369. .removeAttr('required')
  370. .attr('data-required', 'required')
  371. ;
  372. errors.hide();
  373. } else {
  374. row
  375. .find('[data-required]')
  376. .attr('required', 'required')
  377. ;
  378. errors.show();
  379. }
  380. },
  381. setup_tree_view: function(subject) {
  382. Admin.log('[core|setup_tree_view] setup tree view', subject);
  383. jQuery('ul.js-treeview', subject).treeView();
  384. },
  385. /** Return the width for simple and sortable select2 element **/
  386. get_select2_width: function(element){
  387. var ereg = /width:(auto|(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc)))/i;
  388. // this code is an adaptation of select2 code (initContainerWidth function)
  389. var style = element.attr('style');
  390. //console.log("main style", style);
  391. if (style !== undefined) {
  392. var attrs = style.split(';');
  393. for (i = 0, l = attrs.length; i < l; i = i + 1) {
  394. var matches = attrs[i].replace(/\s/g, '').match(ereg);
  395. if (matches !== null && matches.length >= 1)
  396. return matches[1];
  397. }
  398. }
  399. style = element.css('width');
  400. if (style.indexOf("%") > 0) {
  401. return style;
  402. }
  403. return '100%';
  404. },
  405. setup_sortable_select2: function(subject, data) {
  406. var transformedData = [];
  407. for (var i = 0 ; i < data.length ; i++) {
  408. transformedData[i] = {id: data[i].data, text: data[i].label};
  409. }
  410. subject.select2({
  411. width: function(){
  412. return Admin.get_select2_width(this.element);
  413. },
  414. dropdownAutoWidth: true,
  415. data: transformedData,
  416. multiple: true
  417. });
  418. subject.select2("container").find("ul.select2-choices").sortable({
  419. containment: 'parent',
  420. start: function () {
  421. subject.select2("onSortStart");
  422. },
  423. update: function () {
  424. subject.select2("onSortEnd");
  425. }
  426. });
  427. // On form submit, transform value to match what is expected by server
  428. subject.parents('form:first').submit(function (event) {
  429. var values = subject.val().split(',');
  430. var baseName = subject.attr('name');
  431. baseName = baseName.substring(0, baseName.length-1);
  432. for (var i=0; i<values.length; i++) {
  433. jQuery('<input>')
  434. .attr('type', 'hidden')
  435. .attr('name', baseName+i+']')
  436. .val(values[i])
  437. .appendTo(subject.parents('form:first'));
  438. }
  439. subject.remove();
  440. });
  441. },
  442. setup_sticky_elements: function(subject) {
  443. if (window.SONATA_CONFIG && window.SONATA_CONFIG.USE_STICKYFORMS) {
  444. Admin.log('[core|setup_sticky_elements] setup sticky elements on', subject);
  445. var wrapper = jQuery(subject).find('.content-wrapper');
  446. var navbar = jQuery(wrapper).find('nav.navbar');
  447. var footer = jQuery(wrapper).find('.sonata-ba-form-actions');
  448. if (navbar.length) {
  449. new Waypoint.Sticky({
  450. element: navbar[0],
  451. offset: 50,
  452. handler: function( direction ) {
  453. if (direction == 'up') {
  454. jQuery(navbar).width('auto');
  455. } else {
  456. jQuery(navbar).width(jQuery(wrapper).outerWidth());
  457. }
  458. }
  459. });
  460. }
  461. if (footer.length) {
  462. new Waypoint({
  463. element: wrapper[0],
  464. offset: 'bottom-in-view',
  465. handler: function(direction) {
  466. var position = jQuery('.sonata-ba-form form > .row').outerHeight() + jQuery(footer).outerHeight() - 2;
  467. if (position < jQuery(footer).offset().top) {
  468. jQuery(footer).removeClass('stuck');
  469. }
  470. if (direction == 'up') {
  471. jQuery(footer).addClass('stuck');
  472. }
  473. }
  474. });
  475. }
  476. Admin.handleScroll(footer, navbar, wrapper);
  477. }
  478. },
  479. handleScroll: function(footer, navbar, wrapper) {
  480. if (footer.length && jQuery(window).scrollTop() + jQuery(window).height() != jQuery(document).height()) {
  481. jQuery(footer).addClass('stuck');
  482. }
  483. jQuery(window).scroll(
  484. Admin.debounce(function() {
  485. if (footer.length && jQuery(window).scrollTop() + jQuery(window).height() == jQuery(document).height()) {
  486. jQuery(footer).removeClass('stuck');
  487. }
  488. if (navbar.length && jQuery(window).scrollTop() === 0) {
  489. jQuery(navbar).removeClass('stuck');
  490. }
  491. }, 250)
  492. );
  493. jQuery('body').on('expanded.pushMenu collapsed.pushMenu', function() {
  494. Admin.handleResize(footer, navbar, wrapper);
  495. });
  496. jQuery(window).resize(
  497. Admin.debounce(function() {
  498. Admin.handleResize(footer, navbar, wrapper);
  499. }, 250)
  500. );
  501. },
  502. handleResize: function(footer, navbar, wrapper) {
  503. setTimeout(function() {
  504. if (navbar.length && jQuery(navbar).hasClass('stuck')) {
  505. jQuery(navbar).width(jQuery(wrapper).outerWidth());
  506. }
  507. if (footer.length && jQuery(footer).hasClass('stuck')) {
  508. jQuery(footer).width(jQuery(wrapper).outerWidth());
  509. }
  510. }, 350); // the animation take 0.3s to execute, so we have to take the width, just after the animation ended
  511. },
  512. // http://davidwalsh.name/javascript-debounce-function
  513. debounce: function (func, wait, immediate) {
  514. var timeout;
  515. return function() {
  516. var context = this,
  517. args = arguments;
  518. var later = function() {
  519. timeout = null;
  520. if (!immediate) {
  521. func.apply(context, args);
  522. }
  523. };
  524. var callNow = immediate && !timeout;
  525. clearTimeout(timeout);
  526. timeout = setTimeout(later, wait);
  527. if (callNow) {
  528. func.apply(context, args);
  529. }
  530. };
  531. }
  532. };