Admin.js 23 KB

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