Admin.js 22 KB

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