403 lines
12 KiB
JavaScript
Raw Normal View History

import $ from 'jQuery';
import i18n from 'i18n';
require('../../../thirdparty/jquery-ui/jquery-ui.js');
require('../../../thirdparty/jquery-entwine/dist/jquery.entwine-dist.js');
API Use Webpack The bundle is generated by running “webpack” directly - gulp is no longer needed as an intermediary. The resulting config is a lot shorter, although more configuration is pushed into lib.js. Modules are shared between javascript files as global variables. Although this global state pollution is a bit messy, I don’t think it’s practically any worse than the previous state, and it highlights the heavy coupling between the different packages we have in place. Reducing the width of the coupling between the core javascript and add-on modules would probably be a better way of dealing with this than replacing global variables with some other kind of global state. The web pack execution seems roughly twice as fast - if I clear out my framework/client/dist/js folder, it takes 13.3s to rebuild. However, it’s not rebuilding other files inside dist, only the bundle files. CSS files are now included from javascript and incorporated into bundle.css by the webpack. Although the style-loader is helpful in some dev workflows (it allows live reload), it introduces a flash of unstyled content which makes it inappropriate for production. Instead ExtractTextPlugin is used to write all the aggregated CSS into a single bundle.css file. A style-loader-based configuration could be introduced for dev environments, if we make use of the webpack live reloader in the future. Note that the following features have been removed as they don't appear to be necessary when using Webpack: - UMD module generation - thirdparty dist file copying LeftAndMain.js deps: Without it, ssui.core.js gets loaded too late, which leads e.g. to buttons being initialised without this added behaviour.
2016-08-21 13:17:50 +12:00
// TODO Enable once https://github.com/webpack/extract-text-webpack-plugin/issues/179 is resolved. Included in bundle.scss for now.
// require('../styles/legacy/GridField.scss');
API Use Webpack The bundle is generated by running “webpack” directly - gulp is no longer needed as an intermediary. The resulting config is a lot shorter, although more configuration is pushed into lib.js. Modules are shared between javascript files as global variables. Although this global state pollution is a bit messy, I don’t think it’s practically any worse than the previous state, and it highlights the heavy coupling between the different packages we have in place. Reducing the width of the coupling between the core javascript and add-on modules would probably be a better way of dealing with this than replacing global variables with some other kind of global state. The web pack execution seems roughly twice as fast - if I clear out my framework/client/dist/js folder, it takes 13.3s to rebuild. However, it’s not rebuilding other files inside dist, only the bundle files. CSS files are now included from javascript and incorporated into bundle.css by the webpack. Although the style-loader is helpful in some dev workflows (it allows live reload), it introduces a flash of unstyled content which makes it inappropriate for production. Instead ExtractTextPlugin is used to write all the aggregated CSS into a single bundle.css file. A style-loader-based configuration could be introduced for dev environments, if we make use of the webpack live reloader in the future. Note that the following features have been removed as they don't appear to be necessary when using Webpack: - UMD module generation - thirdparty dist file copying LeftAndMain.js deps: Without it, ssui.core.js gets loaded too late, which leads e.g. to buttons being initialised without this added behaviour.
2016-08-21 13:17:50 +12:00
$.entwine('ss', function($) {
2016-07-01 13:37:29 +12:00
$('.grid-field').entwine({
/**
* @param {Object} Additional options for jQuery.ajax() call
* @param {successCallback} callback to call after reloading succeeded.
*/
reload: function(ajaxOpts, successCallback) {
var self = this, form = this.closest('form'),
focusedElName = this.find(':input:focus').attr('name'), // Save focused element for restoring after refresh
data = form.find(':input').serializeArray();
if(!ajaxOpts) ajaxOpts = {};
if(!ajaxOpts.data) ajaxOpts.data = [];
ajaxOpts.data = ajaxOpts.data.concat(data);
// Include any GET parameters from the current URL, as the view state might depend on it.
// For example, a list prefiltered through external search criteria might be passed to GridField.
if(window.location.search) {
ajaxOpts.data = window.location.search.replace(/^\?/, '') + '&' + $.param(ajaxOpts.data);
}
form.addClass('loading');
$.ajax($.extend({}, {
headers: {"X-Pjax" : 'CurrentField'},
type: "POST",
url: this.data('url'),
dataType: 'html',
success: function(data) {
// Replace the grid field with response, not the form.
// TODO Only replaces all its children, to avoid replacing the current scope
// of the executing method. Means that it doesn't retrigger the onmatch() on the main container.
self.empty().append($(data).children());
// Refocus previously focused element. Useful e.g. for finding+adding
// multiple relationships via keyboard.
if(focusedElName) self.find(':input[name="' + focusedElName + '"]').focus();
// Update filter
if(self.find('.filter-header').length) {
var content;
if(ajaxOpts.data[0].filter=="show") {
content = '<span class="non-sortable"></span>';
self.addClass('show-filter').find('.filter-header').show();
} else {
2016-07-01 13:37:29 +12:00
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>';
self.removeClass('show-filter').find('.filter-header').hide();
}
self.find('.sortable-header th:last').html(content);
}
form.removeClass('loading');
if(successCallback) successCallback.apply(this, arguments);
self.trigger('reload', self);
},
error: function(e) {
alert(i18n._t('GRIDFIELD.ERRORINTRANSACTION'));
form.removeClass('loading');
}
}, ajaxOpts));
},
showDetailView: function(url) {
window.location.href = url;
},
getItems: function() {
return this.find('.ss-gridfield-item');
},
/**
* @param {String}
* @param {Mixed}
*/
setState: function(k, v) {
var state = this.getState();
state[k] = v;
this.find(':input[name="' + this.data('name') + '[GridState]"]').val(JSON.stringify(state));
},
/**
* @return {Object}
*/
getState: function() {
return JSON.parse(this.find(':input[name="' + this.data('name') + '[GridState]"]').val());
}
});
2016-07-01 13:37:29 +12:00
$('.grid-field *').entwine({
getGridField: function() {
2016-07-01 13:37:29 +12:00
return this.closest('.grid-field');
}
});
2016-07-01 13:37:29 +12:00
$('.grid-field :button[name=showFilter]').entwine({
onclick: function(e) {
$('.filter-header')
.show('slow') // animate visibility
.find(':input:first').focus(); // focus first search field
2016-07-01 13:37:29 +12:00
this.closest('.grid-field').addClass('show-filter');
this.parent().html('<span class="non-sortable"></span>');
e.preventDefault();
}
});
2016-07-01 13:37:29 +12:00
$('.grid-field .ss-gridfield-item').entwine({
onclick: function(e) {
if($(e.target).closest('.action').length) {
this._super(e);
return false;
}
var editLink = this.find('.edit-link');
if(editLink.length) this.getGridField().showDetailView(editLink.prop('href'));
},
onmouseover: function() {
if(this.find('.edit-link').length) this.css('cursor', 'pointer');
},
onmouseout: function() {
this.css('cursor', 'default');
}
});
2016-07-01 13:37:29 +12:00
$('.grid-field .action:button').entwine({
onclick: function(e){
var filterState='show'; //filterstate should equal current state.
// If the button is disabled, do nothing.
if (this.button('option', 'disabled')) {
e.preventDefault();
return;
}
2016-07-01 13:37:29 +12:00
if(this.hasClass('ss-gridfield-button-close') || !(this.closest('.grid-field').hasClass('show-filter'))){
filterState='hidden';
}
this.getGridField().reload({data: [{name: this.attr('name'), value: this.val(), filter: filterState}]});
e.preventDefault();
},
/**
* Get the url this action should submit to
*/
actionurl: function() {
var btn = this.closest(':button'), grid = this.getGridField(),
form = this.closest('form'), data = form.find(':input.gridstate').serialize(),
csrf = form.find('input[name="SecurityID"]').val();
// Add current button
data += "&" + encodeURIComponent(btn.attr('name')) + '=' + encodeURIComponent(btn.val());
// Add csrf
if(csrf) {
data += "&SecurityID=" + encodeURIComponent(csrf);
}
// Include any GET parameters from the current URL, as the view
// state might depend on it. For example, a list pre-filtered
// through external search criteria might be passed to GridField.
if(window.location.search) {
data = window.location.search.replace(/^\?/, '') + '&' + data;
}
// decide whether we should use ? or & to connect the URL
var connector = grid.data('url').indexOf('?') == -1 ? '?' : '&';
return $.path.makeUrlAbsolute(
grid.data('url') + connector + data,
$('base').attr('href')
);
}
});
/**
* Don't allow users to submit empty values in grid field auto complete inputs.
*/
2016-07-01 13:37:29 +12:00
$('.grid-field .add-existing-autocompleter').entwine({
onbuttoncreate: function () {
var self = this;
this.toggleDisabled();
this.find('input[type="text"]').on('keyup', function () {
self.toggleDisabled();
});
},
onunmatch: function () {
this.find('input[type="text"]').off('keyup');
},
toggleDisabled: function () {
var $button = this.find('.ss-ui-button'),
$input = this.find('input[type="text"]'),
inputHasValue = $input.val() !== '',
buttonDisabled = $button.is(':disabled');
if ((inputHasValue && buttonDisabled) || (!inputHasValue && !buttonDisabled)) {
$button.button("option", "disabled", !buttonDisabled);
}
}
});
// Covers both tabular delete button, and the button on the detail form
2016-07-01 13:37:29 +12:00
$('.grid-field .grid-field__col-compact .action.gridfield-button-delete, .cms-edit-form .btn-toolbar button.action.action-delete').entwine({
onclick: function(e){
if(!confirm(i18n._t('TABLEFIELD.DELETECONFIRMMESSAGE'))) {
e.preventDefault();
return false;
} else {
this._super(e);
}
}
});
2016-07-01 13:37:29 +12:00
$('.grid-field .action.gridfield-button-print').entwine({
UUID: null,
onmatch: function() {
this._super();
this.setUUID(new Date().getTime());
},
onunmatch: function() {
this._super();
},
onclick: function(e){
var url = this.actionurl();
window.open(url);
e.preventDefault();
return false;
}
});
$('.ss-gridfield-print-iframe').entwine({
onmatch: function(){
this._super();
this.hide().bind('load', function() {
this.focus();
var ifWin = this.contentWindow || this;
ifWin.print();
});
},
onunmatch: function() {
this._super();
}
});
/**
* Prevents actions from causing an ajax reload of the field.
*
* Useful e.g. for actions which rely on HTTP response headers being
* interpreted natively by the browser, like file download triggers.
*/
2016-07-01 13:37:29 +12:00
$('.grid-field .action.no-ajax').entwine({
onclick: function(e){
window.location.href = this.actionurl();
e.preventDefault();
return false;
}
});
2016-07-01 13:37:29 +12:00
$('.grid-field .action-detail').entwine({
onclick: function() {
this.getGridField().showDetailView($(this).prop('href'));
return false;
}
});
/**
* Allows selection of one or more rows in the grid field.
* Purely clientside at the moment.
*/
2016-07-01 13:37:29 +12:00
$('.grid-field[data-selectable]').entwine({
/**
* @return {jQuery} Collection
*/
getSelectedItems: function() {
return this.find('.ss-gridfield-item.ui-selected');
},
/**
* @return {Array} Of record IDs
*/
getSelectedIDs: function() {
return $.map(this.getSelectedItems(), function(el) {return $(el).data('id');});
}
});
2016-07-01 13:37:29 +12:00
$('.grid-field[data-selectable] .ss-gridfield-items').entwine({
onadd: function() {
this._super();
// TODO Limit to single selection
this.selectable();
},
onremove: function() {
this._super();
if (this.data('selectable')) this.selectable('destroy');
}
});
/**
* Catch submission event in filter input fields, and submit the correct button
* rather than the whole form.
*/
2016-07-01 13:37:29 +12:00
$('.grid-field .filter-header :input').entwine({
onmatch: function() {
2016-07-01 13:37:29 +12:00
var filterbtn = this.closest('.extra').find('.ss-gridfield-button-filter'),
resetbtn = this.closest('.extra').find('.ss-gridfield-button-reset');
if(this.val()) {
filterbtn.addClass('filtered');
resetbtn.addClass('filtered');
}
this._super();
},
onunmatch: function() {
this._super();
},
onkeydown: function(e) {
// Skip reset button events, they should trigger default submission
if(this.closest('.ss-gridfield-button-reset').length) return;
2016-07-01 13:37:29 +12:00
var filterbtn = this.closest('.extra').find('.ss-gridfield-button-filter'),
resetbtn = this.closest('.extra').find('.ss-gridfield-button-reset');
if(e.keyCode == '13') {
var btns = this.closest('.filter-header').find('.ss-gridfield-button-filter');
var filterState='show'; //filterstate should equal current state.
2016-07-01 13:37:29 +12:00
if(this.hasClass('ss-gridfield-button-close')||!(this.closest('.grid-field').hasClass('show-filter'))){
filterState='hidden';
}
this.getGridField().reload({data: [{name: btns.attr('name'), value: btns.val(), filter: filterState}]});
return false;
}else{
filterbtn.addClass('hover-alike');
resetbtn.addClass('hover-alike');
}
}
});
2016-07-01 13:37:29 +12:00
$(".grid-field .relation-search").entwine({
onfocusin: function (event) {
this.autocomplete({
source: function(request, response){
var searchField = $(this.element);
var form = $(this.element).closest("form");
$.ajax({
headers: {
"X-Pjax" : 'Partial'
},
dataType: 'json',
type: "GET",
url: $(searchField).data('searchUrl'),
data: encodeURIComponent(searchField.attr('name'))+'='+encodeURIComponent(searchField.val()),
success: response,
error: function(e) {
alert(i18n._t('GRIDFIELD.ERRORINTRANSACTION', 'An error occured while fetching data from the server\n Please try again later.'));
}
});
},
select: function(event, ui) {
var hiddenField = $('<input type="hidden" name="relationID" class="action_gridfield_relationfind" />');
hiddenField.val(ui.item.id);
$(this)
2016-07-01 13:37:29 +12:00
.closest(".grid-field")
.find(".action_gridfield_relationfind")
.replaceWith(hiddenField);
2016-07-01 13:37:29 +12:00
var addbutton = $(this).closest(".grid-field").find(".action_gridfield_relationadd");
if(addbutton.data('button')){
addbutton.button('enable');
} else {
addbutton.removeAttr('disabled');
}
}
});
}
});
2016-07-01 13:37:29 +12:00
$(".grid-field .pagination-page-number input").entwine({
onkeydown: function(event) {
if(event.keyCode == 13) {
var newpage = parseInt($(this).val(), 10);
var gridfield = $(this).getGridField();
gridfield.setState('GridFieldPaginator', {currentPage: newpage});
gridfield.reload();
return false;
}
}
});
});