PageRenderTime 144ms CodeModel.GetById 22ms RepoModel.GetById 3ms app.codeStats 0ms

/client/src/legacy/GridField.js

http://github.com/silverstripe/sapphire
JavaScript | 396 lines | 292 code | 49 blank | 55 comment | 35 complexity | 345b34827691e0e459acf7a0235e0f4d MD5 | raw file
Possible License(s): BSD-3-Clause, MIT, CC-BY-3.0, GPL-2.0, AGPL-1.0, LGPL-2.1
  1. import $ from 'jQuery';
  2. import i18n from 'i18n';
  3. $.entwine('ss', function($) {
  4. $('.grid-field').entwine({
  5. /**
  6. * @param {Object} Additional options for jQuery.ajax() call
  7. * @param {successCallback} callback to call after reloading succeeded.
  8. */
  9. reload: function(ajaxOpts, successCallback) {
  10. var self = this, form = this.closest('form'),
  11. focusedElName = this.find(':input:focus').attr('name'), // Save focused element for restoring after refresh
  12. data = form.find(':input').serializeArray();
  13. if(!ajaxOpts) ajaxOpts = {};
  14. if(!ajaxOpts.data) ajaxOpts.data = [];
  15. ajaxOpts.data = ajaxOpts.data.concat(data);
  16. // Include any GET parameters from the current URL, as the view state might depend on it.
  17. // For example, a list prefiltered through external search criteria might be passed to GridField.
  18. if(window.location.search) {
  19. ajaxOpts.data = window.location.search.replace(/^\?/, '') + '&' + $.param(ajaxOpts.data);
  20. }
  21. form.addClass('loading');
  22. $.ajax($.extend({}, {
  23. headers: {"X-Pjax" : 'CurrentField'},
  24. type: "POST",
  25. url: this.data('url'),
  26. dataType: 'html',
  27. success: function(data) {
  28. // Replace the grid field with response, not the form.
  29. // TODO Only replaces all its children, to avoid replacing the current scope
  30. // of the executing method. Means that it doesn't retrigger the onmatch() on the main container.
  31. self.empty().append($(data).children());
  32. // Refocus previously focused element. Useful e.g. for finding+adding
  33. // multiple relationships via keyboard.
  34. if(focusedElName) self.find(':input[name="' + focusedElName + '"]').focus();
  35. // Update filter
  36. if(self.find('.filter-header').length) {
  37. var content;
  38. if(ajaxOpts.data[0].filter=="show") {
  39. content = '<span class="non-sortable"></span>';
  40. self.addClass('show-filter').find('.filter-header').show();
  41. } else {
  42. content = '<button type="button" name="showFilter" class="btn font-icon-search btn--no-text btn--icon-large grid-field__filter-open ss-gridfield-button-filter trigger"></button>';
  43. self.removeClass('show-filter').find('.filter-header').hide();
  44. }
  45. self.find('.sortable-header th:last').html(content);
  46. }
  47. form.removeClass('loading');
  48. if(successCallback) successCallback.apply(this, arguments);
  49. self.trigger('reload', self);
  50. },
  51. error: function(e) {
  52. alert(i18n._t('GRIDFIELD.ERRORINTRANSACTION'));
  53. form.removeClass('loading');
  54. }
  55. }, ajaxOpts));
  56. },
  57. showDetailView: function(url) {
  58. window.location.href = url;
  59. },
  60. getItems: function() {
  61. return this.find('.ss-gridfield-item');
  62. },
  63. /**
  64. * @param {String}
  65. * @param {Mixed}
  66. */
  67. setState: function(k, v) {
  68. var state = this.getState();
  69. state[k] = v;
  70. this.find(':input[name="' + this.data('name') + '[GridState]"]').val(JSON.stringify(state));
  71. },
  72. /**
  73. * @return {Object}
  74. */
  75. getState: function() {
  76. return JSON.parse(this.find(':input[name="' + this.data('name') + '[GridState]"]').val());
  77. }
  78. });
  79. $('.grid-field *').entwine({
  80. getGridField: function() {
  81. return this.closest('.grid-field');
  82. }
  83. });
  84. $('.grid-field :button[name=showFilter]').entwine({
  85. onclick: function(e) {
  86. $('.filter-header')
  87. .show('slow') // animate visibility
  88. .find(':input:first').focus(); // focus first search field
  89. this.closest('.grid-field').addClass('show-filter');
  90. this.parent().html('<span class="non-sortable"></span>');
  91. e.preventDefault();
  92. }
  93. });
  94. $('.grid-field .ss-gridfield-item').entwine({
  95. onclick: function(e) {
  96. if($(e.target).closest('.action').length) {
  97. this._super(e);
  98. return false;
  99. }
  100. var editLink = this.find('.edit-link');
  101. if(editLink.length) this.getGridField().showDetailView(editLink.prop('href'));
  102. },
  103. onmouseover: function() {
  104. if(this.find('.edit-link').length) this.css('cursor', 'pointer');
  105. },
  106. onmouseout: function() {
  107. this.css('cursor', 'default');
  108. }
  109. });
  110. $('.grid-field .action:button').entwine({
  111. onclick: function(e){
  112. var filterState='show'; //filterstate should equal current state.
  113. // If the button is disabled, do nothing.
  114. if (this.button('option', 'disabled')) {
  115. e.preventDefault();
  116. return;
  117. }
  118. if(this.hasClass('ss-gridfield-button-close') || !(this.closest('.grid-field').hasClass('show-filter'))){
  119. filterState='hidden';
  120. }
  121. this.getGridField().reload({data: [{name: this.attr('name'), value: this.val(), filter: filterState}]});
  122. e.preventDefault();
  123. },
  124. /**
  125. * Get the url this action should submit to
  126. */
  127. actionurl: function() {
  128. var btn = this.closest(':button'), grid = this.getGridField(),
  129. form = this.closest('form'), data = form.find(':input.gridstate').serialize(),
  130. csrf = form.find('input[name="SecurityID"]').val();
  131. // Add current button
  132. data += "&" + encodeURIComponent(btn.attr('name')) + '=' + encodeURIComponent(btn.val());
  133. // Add csrf
  134. if(csrf) {
  135. data += "&SecurityID=" + encodeURIComponent(csrf);
  136. }
  137. // Include any GET parameters from the current URL, as the view
  138. // state might depend on it. For example, a list pre-filtered
  139. // through external search criteria might be passed to GridField.
  140. if(window.location.search) {
  141. data = window.location.search.replace(/^\?/, '') + '&' + data;
  142. }
  143. // decide whether we should use ? or & to connect the URL
  144. var connector = grid.data('url').indexOf('?') == -1 ? '?' : '&';
  145. return $.path.makeUrlAbsolute(
  146. grid.data('url') + connector + data,
  147. $('base').attr('href')
  148. );
  149. }
  150. });
  151. /**
  152. * Don't allow users to submit empty values in grid field auto complete inputs.
  153. */
  154. $('.grid-field .add-existing-autocompleter').entwine({
  155. onbuttoncreate: function () {
  156. var self = this;
  157. this.toggleDisabled();
  158. this.find('input[type="text"]').on('keyup', function () {
  159. self.toggleDisabled();
  160. });
  161. },
  162. onunmatch: function () {
  163. this.find('input[type="text"]').off('keyup');
  164. },
  165. toggleDisabled: function () {
  166. var $button = this.find('.ss-ui-button'),
  167. $input = this.find('input[type="text"]'),
  168. inputHasValue = $input.val() !== '',
  169. buttonDisabled = $button.is(':disabled');
  170. if ((inputHasValue && buttonDisabled) || (!inputHasValue && !buttonDisabled)) {
  171. $button.button("option", "disabled", !buttonDisabled);
  172. }
  173. }
  174. });
  175. // Covers both tabular delete button, and the button on the detail form
  176. $('.grid-field .grid-field__col-compact .action.gridfield-button-delete, .cms-edit-form .btn-toolbar button.action.action-delete').entwine({
  177. onclick: function(e){
  178. if(!confirm(i18n._t('TABLEFIELD.DELETECONFIRMMESSAGE'))) {
  179. e.preventDefault();
  180. return false;
  181. } else {
  182. this._super(e);
  183. }
  184. }
  185. });
  186. $('.grid-field .action.gridfield-button-print').entwine({
  187. UUID: null,
  188. onmatch: function() {
  189. this._super();
  190. this.setUUID(new Date().getTime());
  191. },
  192. onunmatch: function() {
  193. this._super();
  194. },
  195. onclick: function(e){
  196. var url = this.actionurl();
  197. window.open(url);
  198. e.preventDefault();
  199. return false;
  200. }
  201. });
  202. $('.ss-gridfield-print-iframe').entwine({
  203. onmatch: function(){
  204. this._super();
  205. this.hide().bind('load', function() {
  206. this.focus();
  207. var ifWin = this.contentWindow || this;
  208. ifWin.print();
  209. });
  210. },
  211. onunmatch: function() {
  212. this._super();
  213. }
  214. });
  215. /**
  216. * Prevents actions from causing an ajax reload of the field.
  217. *
  218. * Useful e.g. for actions which rely on HTTP response headers being
  219. * interpreted natively by the browser, like file download triggers.
  220. */
  221. $('.grid-field .action.no-ajax').entwine({
  222. onclick: function(e){
  223. window.location.href = this.actionurl();
  224. e.preventDefault();
  225. return false;
  226. }
  227. });
  228. $('.grid-field .action-detail').entwine({
  229. onclick: function() {
  230. this.getGridField().showDetailView($(this).prop('href'));
  231. return false;
  232. }
  233. });
  234. /**
  235. * Allows selection of one or more rows in the grid field.
  236. * Purely clientside at the moment.
  237. */
  238. $('.grid-field[data-selectable]').entwine({
  239. /**
  240. * @return {jQuery} Collection
  241. */
  242. getSelectedItems: function() {
  243. return this.find('.ss-gridfield-item.ui-selected');
  244. },
  245. /**
  246. * @return {Array} Of record IDs
  247. */
  248. getSelectedIDs: function() {
  249. return $.map(this.getSelectedItems(), function(el) {return $(el).data('id');});
  250. }
  251. });
  252. $('.grid-field[data-selectable] .ss-gridfield-items').entwine({
  253. onadd: function() {
  254. this._super();
  255. // TODO Limit to single selection
  256. this.selectable();
  257. },
  258. onremove: function() {
  259. this._super();
  260. if (this.data('selectable')) this.selectable('destroy');
  261. }
  262. });
  263. /**
  264. * Catch submission event in filter input fields, and submit the correct button
  265. * rather than the whole form.
  266. */
  267. $('.grid-field .filter-header :input').entwine({
  268. onmatch: function() {
  269. var filterbtn = this.closest('.extra').find('.ss-gridfield-button-filter'),
  270. resetbtn = this.closest('.extra').find('.ss-gridfield-button-reset');
  271. if(this.val()) {
  272. filterbtn.addClass('filtered');
  273. resetbtn.addClass('filtered');
  274. }
  275. this._super();
  276. },
  277. onunmatch: function() {
  278. this._super();
  279. },
  280. onkeydown: function(e) {
  281. // Skip reset button events, they should trigger default submission
  282. if(this.closest('.ss-gridfield-button-reset').length) return;
  283. var filterbtn = this.closest('.extra').find('.ss-gridfield-button-filter'),
  284. resetbtn = this.closest('.extra').find('.ss-gridfield-button-reset');
  285. if(e.keyCode == '13') {
  286. var btns = this.closest('.filter-header').find('.ss-gridfield-button-filter');
  287. var filterState='show'; //filterstate should equal current state.
  288. if(this.hasClass('ss-gridfield-button-close')||!(this.closest('.grid-field').hasClass('show-filter'))){
  289. filterState='hidden';
  290. }
  291. this.getGridField().reload({data: [{name: btns.attr('name'), value: btns.val(), filter: filterState}]});
  292. return false;
  293. }else{
  294. filterbtn.addClass('hover-alike');
  295. resetbtn.addClass('hover-alike');
  296. }
  297. }
  298. });
  299. $(".grid-field .relation-search").entwine({
  300. onfocusin: function (event) {
  301. this.autocomplete({
  302. source: function(request, response){
  303. var searchField = $(this.element);
  304. var form = $(this.element).closest("form");
  305. $.ajax({
  306. headers: {
  307. "X-Pjax" : 'Partial'
  308. },
  309. dataType: 'json',
  310. type: "GET",
  311. url: $(searchField).data('searchUrl'),
  312. data: encodeURIComponent(searchField.attr('name'))+'='+encodeURIComponent(searchField.val()),
  313. success: response,
  314. error: function(e) {
  315. alert(i18n._t('GRIDFIELD.ERRORINTRANSACTION', 'An error occured while fetching data from the server\n Please try again later.'));
  316. }
  317. });
  318. },
  319. select: function(event, ui) {
  320. var hiddenField = $('<input type="hidden" name="relationID" class="action_gridfield_relationfind" />');
  321. hiddenField.val(ui.item.id);
  322. $(this)
  323. .closest(".grid-field")
  324. .find(".action_gridfield_relationfind")
  325. .replaceWith(hiddenField);
  326. var addbutton = $(this).closest(".grid-field").find(".action_gridfield_relationadd");
  327. if(addbutton.data('button')){
  328. addbutton.button('enable');
  329. } else {
  330. addbutton.removeAttr('disabled');
  331. }
  332. }
  333. });
  334. }
  335. });
  336. $(".grid-field .pagination-page-number input").entwine({
  337. onkeydown: function(event) {
  338. if(event.keyCode == 13) {
  339. var newpage = parseInt($(this).val(), 10);
  340. var gridfield = $(this).getGridField();
  341. gridfield.setState('GridFieldPaginator', {currentPage: newpage});
  342. gridfield.reload();
  343. return false;
  344. }
  345. }
  346. });
  347. });