2875 lines
100 KiB
JavaScript
Vendored
2875 lines
100 KiB
JavaScript
Vendored
(function ($) {
|
|
|
|
/**
|
|
* For the item form transition
|
|
*/
|
|
ko.bindingHandlers.itemTransition = {
|
|
init: function (element, valueAccessor, allBindingsAccessor, viewModel, context) {
|
|
var $element = $(element),
|
|
viewModel = context.$root,
|
|
$child = $element.find('.item_edit'),
|
|
$tableContainer = $('div.table_container'),
|
|
expandWidth = viewModel.expandWidth();
|
|
|
|
//the lastItem gets reset to null when the form is closed. This way we can draw the table properly initially
|
|
//so that it doesn't keep reopening.
|
|
if (viewModel.lastItem === null) {
|
|
$tableContainer.css('margin-right', 290);
|
|
$element.hide();
|
|
$child.css('marginLeft', expandWidth + 2);
|
|
}
|
|
else {
|
|
$tableContainer.css('margin-right', expandWidth + 5);
|
|
$child.css('marginLeft', 2);
|
|
}
|
|
},
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel, context) {
|
|
var $element = $(element),
|
|
viewModel = context.$root,
|
|
$child = $element.find('.item_edit'),
|
|
$tableContainer = $('div.table_container'),
|
|
expandWidth = viewModel.expandWidth();
|
|
|
|
//if the value is false, we want to hide the form, otherwise show it
|
|
if (!valueAccessor()) {
|
|
$child.stop().animate({marginLeft: expandWidth + 2}, 150, function () {
|
|
$element.hide();
|
|
});
|
|
|
|
$tableContainer.stop().animate({marginRight: 290}, 150, function () {
|
|
window.admin.resizePage();
|
|
});
|
|
}
|
|
else {
|
|
if (viewModel.lastItem === null) {
|
|
$element.show();
|
|
$child.stop().animate({marginLeft: 2}, 150);
|
|
$tableContainer.stop().animate({marginRight: expandWidth + 5}, 150, function () {
|
|
window.admin.resizePage();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
var select2Defaults = {
|
|
placeholder: adminData.languages['select_options'],
|
|
formatNoMatches: function (term) {
|
|
return adminData.languages['no_results'];
|
|
},
|
|
width: 'resolve',
|
|
allowClear: true
|
|
};
|
|
|
|
//for select2
|
|
ko.bindingHandlers.select2 = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor(),
|
|
defaults = $.extend({}, select2Defaults),
|
|
data;
|
|
|
|
if (options && typeof options === 'object') {
|
|
$.extend(defaults, options);
|
|
}
|
|
|
|
//pull the latest from the list
|
|
if (defaults.data) {
|
|
if ($.isFunction(defaults.data.results)) {
|
|
defaults.data.results = options.data.results();
|
|
}
|
|
|
|
$(element).data('list_data', defaults.data.results);
|
|
|
|
defaults.data = function () {
|
|
return {results: $(element).data('list_data')};
|
|
}
|
|
}
|
|
|
|
//init select2 if it isn't already set up
|
|
if ($(element).data("select2") === undefined || $(element).data("select2") === null) {
|
|
//set the original list data in case we need it for sorting
|
|
$(element).data('original_list_data', [].concat($(element).data('list_data')));
|
|
|
|
$(element).select2(defaults);
|
|
|
|
//if the sort option is set, set up jquery ui sortable
|
|
if (options.sort) {
|
|
$(element).select2('container').find('ul.select2-choices').sortable({
|
|
containment: 'parent',
|
|
start: function () {
|
|
$(element).select2("onSortStart")
|
|
},
|
|
update: function () {
|
|
$(element).select2("onSortEnd")
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
//it's necessary to reorder the options array if the sort is set
|
|
if (options.sort) {
|
|
var listData = $(element).data('list_data'),
|
|
val = $(element).val();
|
|
|
|
//initially we want to reset the list data so we can work with a fresh, alphabetized sort
|
|
$(element).data('list_data', [].concat($(element).data('original_list_data')));
|
|
|
|
//if there is a value for this field, split it and find the relevant items in the array
|
|
if (val) {
|
|
var vals = val.split(','),
|
|
topItems = [],
|
|
allItems = $(element).data('list_data');
|
|
|
|
//iterate over the values
|
|
$.each(vals, function (ind, el) {
|
|
//iterate over all the items to find our value
|
|
$.each(allItems, function (i, e) {
|
|
if (e.id == el) {
|
|
topItems.push(e);
|
|
allItems.splice(i, 1);
|
|
return false;
|
|
}
|
|
});
|
|
});
|
|
|
|
$(element).data('list_data', topItems.concat(allItems));
|
|
}
|
|
}
|
|
|
|
//make sure we're monitoring the change event for page resizing
|
|
$(element).on('change', function () {
|
|
window.admin.resizePage();
|
|
});
|
|
|
|
setTimeout(function () {
|
|
$(element).trigger('change');
|
|
}, 50);
|
|
}
|
|
};
|
|
|
|
var select2RemoteHandler = function (element, valueAccessor, allBindingsAccessor, viewModel, context) {
|
|
var options = valueAccessor(),
|
|
defaults = $.extend({
|
|
minimumInputLength: 1,
|
|
allowClear: true,
|
|
ajax: {
|
|
url: base_url + adminData.model_name + '/update_options',
|
|
dataType: 'json',
|
|
quietMillis: 100,
|
|
type: 'POST',
|
|
data: function (term, page) {
|
|
var data = {
|
|
term: term,
|
|
page: page,
|
|
field: options.field,
|
|
type: options.type,
|
|
constraints: {}
|
|
};
|
|
|
|
if (data.type === 'edit') {
|
|
data.selectedItems = admin.viewModel[data.field]();
|
|
}
|
|
else if (data.type === 'filter') {
|
|
data.selectedItems = admin.filtersViewModel.filters[parseInt(options.filterIndex)].value();
|
|
}
|
|
|
|
//figure out if there are any constraints that we need to send over
|
|
if (options.constraints) {
|
|
$.each(options.constraints, function (ind, el) {
|
|
data.constraints[ind] = admin.viewModel[ind]();
|
|
});
|
|
}
|
|
|
|
return {fields: [data]};
|
|
},
|
|
results: function (returndata, page) {
|
|
var data = {},
|
|
val = $(element).val();
|
|
|
|
//we want to update the autocomplete index so we can show all possibly-selected items
|
|
if (val) {
|
|
$(val.split(',')).each(function (ind, el) {
|
|
data[this] = {
|
|
id: this,
|
|
text: admin.viewModel[options.field + '_autocomplete'][this].text
|
|
};
|
|
});
|
|
}
|
|
|
|
//iterate over the results and put them in the autocomplete array
|
|
$.each(returndata[options.field], function (ind, el) {
|
|
data[el.id] = el;
|
|
});
|
|
|
|
admin.viewModel[options.field + '_autocomplete'] = data;
|
|
|
|
return {
|
|
results: returndata[options.field]
|
|
}
|
|
}
|
|
},
|
|
initSelection: function (element, callback) {
|
|
var data = [],
|
|
val = $(element).val();
|
|
|
|
// If the select2 field has a default value,
|
|
// initSelection will be called before the admin object
|
|
// is correctly initialized.
|
|
if (!val || typeof admin === 'undefined')
|
|
return callback(null);
|
|
|
|
//if this is a multi-select, set up the data as an array
|
|
if (options.multiple) {
|
|
$(element.val().split(',')).each(function (ind, el) {
|
|
if (this in admin.viewModel[options.field + '_autocomplete'])
|
|
data.push({
|
|
id: this,
|
|
text: admin.viewModel[options.field + '_autocomplete'][this].text
|
|
});
|
|
});
|
|
}
|
|
//otherwise make the data a simple object
|
|
else {
|
|
if (val in admin.viewModel[options.field + '_autocomplete'])
|
|
data = {id: val, text: admin.viewModel[options.field + '_autocomplete'][val].text};
|
|
}
|
|
|
|
callback(data);
|
|
}
|
|
}, select2Defaults);
|
|
|
|
if (options && typeof options === 'object') {
|
|
$.extend(defaults, options);
|
|
}
|
|
|
|
//init select2 if it isn't already set up
|
|
if ($(element).data("select2") === undefined || $(element).data("select2") === null) {
|
|
$(element).select2(defaults);
|
|
|
|
//if the sort option is set, set up jquery ui sortable
|
|
if (options.sort) {
|
|
$(element).select2('container').find('ul.select2-choices').sortable({
|
|
containment: 'parent',
|
|
start: function () {
|
|
$(element).select2("onSortStart")
|
|
},
|
|
update: function () {
|
|
$(element).select2("onSortEnd")
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
setTimeout(function () {
|
|
$(element).trigger('change');
|
|
}, 50);
|
|
}
|
|
|
|
//for ajax/remote select2
|
|
ko.bindingHandlers.select2Remote = {
|
|
update: select2RemoteHandler
|
|
};
|
|
|
|
/**
|
|
* The number binding ensures that a value is decimal-like
|
|
*/
|
|
ko.bindingHandlers.number = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor(),
|
|
value = allBindingsAccessor().value(),
|
|
floatVal,
|
|
$element = $(element);
|
|
|
|
//if this is a null or false value, run a parseFloat on it so we can check for isNaN later
|
|
if (value === null || value === false) {
|
|
floatVal = parseFloat(value);
|
|
}
|
|
//else we will try to parse the number using the user-supplied thousands and decimal separators
|
|
else {
|
|
floatVal = parseFloat(value.toString().trim().split(options.thousandsSeparator).join('').split(options.decimalSeparator).join('.'));
|
|
}
|
|
|
|
//if the value is not a number, set the value equal to ''
|
|
if (isNaN(floatVal)) {
|
|
allBindingsAccessor().value(null);
|
|
|
|
//if this is an uneditable field, set the text
|
|
if ($element.hasClass('uneditable'))
|
|
$element.text('');
|
|
//otherwise we know it's an input
|
|
else
|
|
$element.val('');
|
|
}
|
|
//else set up the value up using the accounting library with the user-supplied separators
|
|
else {
|
|
//if this is an uneditable field, set the text
|
|
if ($element.hasClass('uneditable'))
|
|
$element.text(accounting.formatMoney(floatVal, "", options.decimals, options.thousandsSeparator, options.decimalSeparator));
|
|
//otherwise we know it's an input
|
|
else
|
|
$element.val(accounting.formatMoney(floatVal, "", options.decimals, options.thousandsSeparator, options.decimalSeparator));
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The datepicker binding makes sure the jQuery UI datepicker is set for this item
|
|
*/
|
|
ko.bindingHandlers.datepicker = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor();
|
|
|
|
$(element).datepicker({
|
|
dateFormat: options.dateFormat
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The formatDate binding transforms a date string into a formatted date
|
|
*/
|
|
ko.bindingHandlers.formatDate = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor(),
|
|
dateVal = options.value.length === 10 ? options.val + ' 00:00' : options.val;
|
|
|
|
$(element).text($.datepicker.formatDate(options.dateFormat, new Date(options.value)));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The timepicker binding makes sure the jQuery UI timepicker is set for this item
|
|
*/
|
|
ko.bindingHandlers.timepicker = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor(),
|
|
val = allBindingsAccessor().value(),
|
|
date = new Date('01/01/2013 ' + val),
|
|
timeObject = {
|
|
hour: date.getHours(),
|
|
minute: date.getMinutes()
|
|
};
|
|
|
|
if (val)
|
|
$(element).val($.datepicker.formatTime(options.timeFormat, timeObject));
|
|
|
|
$(element).timepicker({
|
|
timeFormat: options.timeFormat
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The formatTime binding transforms a time string into a formatted time
|
|
*/
|
|
ko.bindingHandlers.formatTime = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor(),
|
|
date = new Date('01/01/2012 ' + options.value),
|
|
timeObject = {
|
|
hour: date.getHours(),
|
|
minute: date.getMinutes()
|
|
};
|
|
|
|
$(element).text($.datepicker.formatTime(options.timeFormat, timeObject));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The datetimepicker binding makes sure the jQuery UI datetimepicker is set for this item
|
|
*/
|
|
ko.bindingHandlers.datetimepicker = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor(),
|
|
val = allBindingsAccessor().value(),
|
|
date = new Date(val),
|
|
timeObject = {
|
|
hour: date.getHours(),
|
|
minute: date.getMinutes()
|
|
};
|
|
|
|
if (val && !isNaN(date.getHours())) {
|
|
|
|
var formattedDate = $.datepicker.formatDate(options.dateFormat, date),
|
|
formattedTime = $.datepicker.formatTime(options.timeFormat, timeObject);
|
|
|
|
$(element).val(formattedDate + ' ' + formattedTime);
|
|
}
|
|
|
|
$(element).datetimepicker({
|
|
dateFormat: options.dateFormat,
|
|
timeFormat: options.timeFormat
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The formatTime binding transforms a datetime string into a formatted datetime
|
|
*/
|
|
ko.bindingHandlers.formatDateTime = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor(),
|
|
date = new Date(options.value),
|
|
timeObject = {
|
|
hour: date.getHours(),
|
|
minute: date.getMinutes()
|
|
};
|
|
|
|
if (!isNaN(date.getHours())) {
|
|
var formattedDate = $.datepicker.formatDate(options.dateFormat, date),
|
|
formattedTime = $.datepicker.formatTime(options.timeFormat, timeObject);
|
|
|
|
$(element).text(formattedDate + ' ' + formattedTime);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The characterLimit binding makes sure a text field only has so many characters
|
|
*/
|
|
ko.bindingHandlers.characterLimit = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var limit = valueAccessor(),
|
|
val = allBindingsAccessor().value();
|
|
|
|
val = val === null ? '' : val + '';
|
|
|
|
if (!limit || val === null || val.length < limit)
|
|
return;
|
|
|
|
val = val.substr(0, limit);
|
|
|
|
$(element).val(val);
|
|
allBindingsAccessor().value(val);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The charactersLeft binding fills the element with (#chars allowed - #chars typed)
|
|
*/
|
|
ko.bindingHandlers.charactersLeft = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor(),
|
|
limit = options.limit,
|
|
val = options.value();
|
|
|
|
val = val === null ? '' : val + '';
|
|
|
|
//if the limit is zero, there is no limit
|
|
if (!limit)
|
|
return;
|
|
|
|
//if the value is null, set it to an empty string
|
|
if (val === null)
|
|
val = '';
|
|
|
|
left = limit - val.length;
|
|
|
|
// text = ' character' + (left !== 1 ? 's' : '') + ' left';
|
|
text = (left !== 1 ? adminData.languages['characters_left'] : adminData.languages['character_left']);
|
|
|
|
$(element).text(left + text);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This ensures that a bool field is always a boolean value
|
|
*/
|
|
ko.bindingHandlers.bool = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel, context) {
|
|
var viewModel = context.$root,
|
|
modelVal = viewModel[valueAccessor()]();
|
|
|
|
if (modelVal === '0')
|
|
viewModel[valueAccessor()](false);
|
|
else if (modelVal === '1')
|
|
viewModel[valueAccessor()](true);
|
|
}
|
|
};
|
|
|
|
var editors = {};
|
|
|
|
/**
|
|
* The wysiwyg binding makes the field a ckeditor wysiwyg
|
|
*/
|
|
ko.bindingHandlers.wysiwyg = {
|
|
init: function (element, valueAccessor, allBindingsAccessor, context) {
|
|
var options = valueAccessor(),
|
|
value = ko.utils.unwrapObservable(options.value),
|
|
$element = $(element),
|
|
editor;
|
|
|
|
value = value ? value : '';
|
|
|
|
$element.html(value);
|
|
|
|
if (options.id in editors)
|
|
editor = editors[options.id];
|
|
else {
|
|
$element.ckeditor({
|
|
language: language,
|
|
readOnly: !adminData.edit_fields[context.field_name].editable
|
|
});
|
|
|
|
editor = $element.ckeditorGet();
|
|
editors[options.id] = editor;
|
|
}
|
|
|
|
//when the editor is loaded, we want to resize our page
|
|
editor.on('loaded', function () {
|
|
setTimeout(function () {
|
|
window.admin.resizePage();
|
|
}, 50);
|
|
|
|
editor.on('resize', function () {
|
|
window.admin.resizePage();
|
|
});
|
|
});
|
|
|
|
//wire up the blur event to ensure our observable is properly updated
|
|
editor.focusManager.blur = function () {
|
|
var observable = valueAccessor().value,
|
|
$el = $('#' + options.id);
|
|
|
|
//set the blur attribute to true so we know now to set the editor data in the update method
|
|
$el.data('blur', true);
|
|
|
|
observable($el.val());
|
|
}
|
|
|
|
//handle destroying an editor (based on what jQuery plugin does)
|
|
ko.utils.domNodeDisposal.addDisposeCallback(element, function (test) {
|
|
var editor = editors[options.id];
|
|
|
|
if (editor) {
|
|
editor.destroy();
|
|
delete editors[options.id];
|
|
}
|
|
});
|
|
},
|
|
update: function (element, valueAccessor, allBindingsAccessor, context) {
|
|
//handle programmatic updates to the observable
|
|
var options = valueAccessor(),
|
|
value = ko.utils.unwrapObservable(options.value),
|
|
$element = $(element),
|
|
editor = editors[options.id];
|
|
|
|
value = value ? value : '';
|
|
|
|
//if there isn't a value, set the value immediately
|
|
if (!value) {
|
|
$element.html(value);
|
|
editor.setData(value);
|
|
}
|
|
//otherwise pause for a moment and then set it
|
|
else {
|
|
setTimeout(function () {
|
|
$element.html(value);
|
|
|
|
if ($element.data('blur'))
|
|
$element.removeData('blur');
|
|
else
|
|
editor.setData(value);
|
|
|
|
}, 50);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The markdown binding is attached to the field next a markdown textarea
|
|
*/
|
|
ko.bindingHandlers.markdown = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, context) {
|
|
//handle programmatic updates to the observable
|
|
var value = ko.utils.unwrapObservable(valueAccessor());
|
|
|
|
if (!value) {
|
|
$(element).html(value);
|
|
}
|
|
else {
|
|
$(element).html(markdown.toHTML(value.toString()));
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The enumText binding converts a value and an options array to a "Label (value)" readable format
|
|
*/
|
|
ko.bindingHandlers.enumText = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor(),
|
|
value = options.value,
|
|
enumOptions = options.enumOptions;
|
|
|
|
for (var i = 0; i < enumOptions.length; i++) {
|
|
if (enumOptions[i].id == value) {
|
|
$(element).html(enumOptions[i].text + " (" + value + ")");
|
|
return;
|
|
}
|
|
}
|
|
|
|
$(element).html(value);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* File uploader using plupload
|
|
*/
|
|
ko.bindingHandlers.fileupload = {
|
|
init: function (element, valueAccessor, allBindingsAccessor, viewModel, context) {
|
|
var options = valueAccessor(),
|
|
cacheName = options.field + '_uploader',
|
|
viewModel = context.$root,
|
|
filters = options.image ? [{title: 'Image files', extensions: 'jpg,jpeg,gif,png'}] : [];
|
|
|
|
viewModel[cacheName] = new plupload.Uploader({
|
|
runtimes: 'html5,flash,silverlight,gears,browserplus',
|
|
browse_button: cacheName,
|
|
container: 'edit_field_' + options.field,
|
|
drop_element: cacheName,
|
|
multi_selection: false,
|
|
max_file_size: options.size_limit + 'mb',
|
|
url: options.upload_url,
|
|
flash_swf_url: asset_url + 'js/plupload/js/plupload.flash.swf',
|
|
silverlight_xap_url: asset_url + 'js/plupload/js/plupload.silverlight.xap',
|
|
filters: filters,
|
|
multipart_params: {"_token": window.csrf}
|
|
});
|
|
|
|
viewModel[cacheName].init();
|
|
|
|
viewModel[cacheName].bind('FilesAdded', function (up, files) {
|
|
|
|
viewModel.freezeActions(true);
|
|
|
|
$(files).each(function (i, file) {
|
|
//parent.uploader.removeFile(file);
|
|
|
|
});
|
|
|
|
options.upload_percentage(0);
|
|
options.uploading(true);
|
|
|
|
viewModel[cacheName].start();
|
|
});
|
|
|
|
viewModel[cacheName].bind('UploadProgress', function (up, file) {
|
|
options.upload_percentage(file.percent);
|
|
});
|
|
|
|
viewModel[cacheName].bind('Error', function (up, err) {
|
|
alert(err.message);
|
|
});
|
|
|
|
viewModel[cacheName].bind('FileUploaded', function (up, file, response) {
|
|
var data = JSON.parse(response.response);
|
|
|
|
options.uploading(false);
|
|
|
|
if (data.errors.length === 0) {
|
|
//success
|
|
//iterate over the files until we find it and then set the proper fields
|
|
viewModel[options.field](data.filename);
|
|
} else {
|
|
//error
|
|
alert(data.errors);
|
|
}
|
|
|
|
setTimeout(function () {
|
|
viewModel[cacheName].splice();
|
|
viewModel[cacheName].refresh();
|
|
$('div.plupload').css('z-index', 71);
|
|
viewModel.freezeActions(false);
|
|
admin.resizePage();
|
|
}, 200);
|
|
});
|
|
|
|
$('#' + cacheName).bind('dragenter', function (e) {
|
|
$(this).addClass('drag');
|
|
});
|
|
|
|
$('#' + cacheName).bind('dragleave drop', function (e) {
|
|
$(this).removeClass('drag');
|
|
});
|
|
|
|
//destroy the existing editor if the DOM node is removed
|
|
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
|
|
viewModel[cacheName].destroy();
|
|
});
|
|
},
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel, context) {
|
|
var options = valueAccessor(),
|
|
cacheName = options.field + '_uploader',
|
|
viewModel = context.$root;
|
|
|
|
//hack to get the z-index properly set up
|
|
setTimeout(function () {
|
|
viewModel[cacheName].refresh();
|
|
$('div.plupload').css('z-index', 71);
|
|
}, 200);
|
|
}
|
|
};
|
|
|
|
})(jQuery);
|
|
(function ($) {
|
|
var admin = function () {
|
|
return this.init();
|
|
};
|
|
|
|
//setting up csrf token
|
|
$.ajaxSetup({
|
|
headers: {
|
|
'X-CSRF-TOKEN': window.csrf
|
|
}
|
|
});
|
|
|
|
admin.prototype = {
|
|
|
|
//properties
|
|
|
|
/*
|
|
* Main admin container
|
|
*
|
|
* @type jQuery object
|
|
*/
|
|
$container: null,
|
|
|
|
/*
|
|
* The container for the datatable
|
|
*
|
|
* @type jQuery object
|
|
*/
|
|
$tableContainer: null,
|
|
|
|
/*
|
|
* The data table
|
|
*
|
|
* @type jQuery object
|
|
*/
|
|
$dataTable: null,
|
|
|
|
/*
|
|
* If this is true, the dataTable is scrollable instead of
|
|
* skipping columns at the end
|
|
*
|
|
* @type bool
|
|
*/
|
|
dataTableScrollable: false,
|
|
|
|
/*
|
|
* The pixel points where the columns are hidden
|
|
*
|
|
* @type object
|
|
*/
|
|
columnHidePoints: {},
|
|
|
|
/*
|
|
* If this is true, history.js has started
|
|
*
|
|
* @type bool
|
|
*/
|
|
historyStarted: false,
|
|
|
|
/*
|
|
* Filters view model
|
|
*/
|
|
filtersViewModel: {
|
|
|
|
/* The filters for the current result set
|
|
* array
|
|
*/
|
|
filters: [],
|
|
|
|
/* The options lists for any fields
|
|
* object
|
|
*/
|
|
listOptions: {},
|
|
|
|
/**
|
|
* The options for booleans
|
|
* array
|
|
*/
|
|
boolOptions: [{id: 'true', text: 'true'}, {id: 'false', text: 'false'}]
|
|
},
|
|
|
|
/*
|
|
* KO viewModel
|
|
*/
|
|
viewModel: {
|
|
|
|
/*
|
|
* KO data model
|
|
*/
|
|
model: {},
|
|
|
|
/*
|
|
* If this is true, all the values have been initialized and we can
|
|
*
|
|
* bool
|
|
*/
|
|
initialized: ko.observable(false),
|
|
|
|
/* The model name for this data model
|
|
* string
|
|
*/
|
|
modelName: ko.observable(''),
|
|
|
|
/* The model title for this data model
|
|
* string
|
|
*/
|
|
modelTitle: ko.observable(''),
|
|
|
|
/* The sub title for this data model
|
|
* string
|
|
*/
|
|
subTitle: ko.observable(''),
|
|
|
|
/* The title for single items of this model
|
|
* string
|
|
*/
|
|
modelSingle: ko.observable(''),
|
|
|
|
/* The link (usually front-end) associated with this item
|
|
* string
|
|
*/
|
|
itemLink: ko.observable(null),
|
|
|
|
/* The expand width of the edit area
|
|
* int
|
|
*/
|
|
expandWidth: ko.observable(null),
|
|
|
|
/* The primary key value for this model
|
|
* string
|
|
*/
|
|
primaryKey: 'id',
|
|
|
|
/* The rows of the current result set
|
|
* array
|
|
*/
|
|
rows: ko.observableArray(),
|
|
|
|
/* The number of rows per page
|
|
* int
|
|
*/
|
|
rowsPerPage: ko.observable(20),
|
|
|
|
/* The options (1-100 ...set up in init method) for the rows per page
|
|
* array
|
|
*/
|
|
rowsPerPageOptions: [],
|
|
|
|
/* The columns for the current data model
|
|
* array
|
|
*/
|
|
columns: ko.observableArray(),
|
|
|
|
/* The options lists for any fields
|
|
* object
|
|
*/
|
|
listOptions: {},
|
|
|
|
/* The current sort options
|
|
* object
|
|
*/
|
|
sortOptions: {
|
|
field: ko.observable(),
|
|
direction: ko.observable()
|
|
},
|
|
|
|
/* The current pagination options
|
|
* object
|
|
*/
|
|
pagination: {
|
|
page: ko.observable(),
|
|
last: ko.observable(),
|
|
total: ko.observable(),
|
|
per_page: ko.observable(),
|
|
isFirst: true,
|
|
isLast: false,
|
|
},
|
|
|
|
/* The original edit fields array
|
|
* array
|
|
*/
|
|
originalEditFields: [],
|
|
|
|
/* The original data when fetched from the server initially
|
|
* object
|
|
*/
|
|
originalData: {},
|
|
|
|
/* The model edit fields
|
|
* array
|
|
*/
|
|
editFields: ko.observableArray(),
|
|
|
|
/* The id of the active item. If it's null, there is no active item. If it's 0, the active item is new
|
|
* mixed (null, int)
|
|
*/
|
|
activeItem: ko.observable(null),
|
|
|
|
/* The id of the last active item. This is set to null when an item is closed. 0 is new.
|
|
* mixed (null, int)
|
|
*/
|
|
lastItem: null,
|
|
|
|
/* If this is set to true, the loading screen will be visible
|
|
* bool
|
|
*/
|
|
loadingItem: ko.observable(false),
|
|
|
|
/* The id of the item currently being loaded
|
|
* int
|
|
*/
|
|
itemLoadingId: ko.observable(null),
|
|
|
|
/* If this is set to true, the row loading screen will be visible
|
|
* bool
|
|
*/
|
|
loadingRows: ko.observable(false),
|
|
|
|
/* The id of the rows currently being loaded
|
|
* int
|
|
*/
|
|
rowLoadingId: 0,
|
|
|
|
/* If this is set to true, the form becomes uneditable
|
|
* bool
|
|
*/
|
|
freezeForm: ko.observable(false),
|
|
|
|
/* If this is set to true, the action buttons on the form cannot be accessed
|
|
* bool
|
|
*/
|
|
freezeActions: ko.observable(false),
|
|
|
|
/* If this is set to true, the relationship constraints won't update
|
|
* bool
|
|
*/
|
|
freezeConstraints: false,
|
|
|
|
/* The current constraints queue
|
|
* object
|
|
*/
|
|
constraintsQueue: {},
|
|
|
|
/* If this is set to true, the relationship constraints queue won't process
|
|
* bool
|
|
*/
|
|
holdConstraintsQueue: true,
|
|
|
|
/* If custom actions are supplied, they are stored here
|
|
* array
|
|
*/
|
|
actions: ko.observableArray(),
|
|
|
|
/* If custom global actions are supplied, they are stored here
|
|
* array
|
|
*/
|
|
globalActions: ko.observableArray(),
|
|
|
|
/* Holds the per-action permissions
|
|
* object
|
|
*/
|
|
actionPermissions: {},
|
|
|
|
/* The languages array holds text for the current language
|
|
* object
|
|
*/
|
|
languages: {},
|
|
|
|
/* The status message and the type ('', 'success', 'error')
|
|
* strings
|
|
*/
|
|
statusMessage: ko.observable(''),
|
|
statusMessageType: ko.observable(''),
|
|
|
|
/* The global status message and the type ('', 'success', 'error')
|
|
* strings
|
|
*/
|
|
globalStatusMessage: ko.observable(''),
|
|
globalStatusMessageType: ko.observable(''),
|
|
|
|
/**
|
|
* Saves the item with the current settings. If id is 0, the server interprets it as a new item
|
|
*/
|
|
saveItem: function (norefresh) {
|
|
var self = this,
|
|
saveData = ko.mapping.toJS(self);
|
|
|
|
saveData._token = csrf;
|
|
|
|
//if this is a new item, delete the primary key from the data array
|
|
if (!saveData[self.primaryKey])
|
|
delete saveData[self.primaryKey];
|
|
|
|
//iterate over the edit fields and ensure that the belongs_to relationships are false if they are an empty string
|
|
$.each(self.editFields(), function (ind, field) {
|
|
if (field.relationship && !field.external && saveData[field.field_name] === '') {
|
|
saveData[field.field_name] = false;
|
|
}
|
|
});
|
|
|
|
self.statusMessage(self.languages['saving']).statusMessageType('');
|
|
self.freezeForm(true);
|
|
|
|
$.ajax({
|
|
url: base_url + self.modelName() + '/' + self[self.primaryKey]() + '/save',
|
|
data: saveData,
|
|
dataType: 'json',
|
|
type: 'POST',
|
|
complete: function () {
|
|
self.freezeForm(false);
|
|
window.admin.resizePage();
|
|
},
|
|
success: function (response) {
|
|
if (response.success) {
|
|
self.statusMessage(self.languages['saved']).statusMessageType('success');
|
|
self.updateRows();
|
|
self.updateSelfRelationships();
|
|
|
|
if (norefresh) return;
|
|
|
|
self.setData(response.data);
|
|
|
|
setTimeout(function () {
|
|
History.pushState({modelName: self.modelName()}, document.title, route + self.modelName());
|
|
}, 200);
|
|
}
|
|
else
|
|
self.statusMessage(response.errors).statusMessageType('error');
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Deletes the active item
|
|
*/
|
|
deleteItem: function (root, event, key) {
|
|
var self = root;
|
|
|
|
swal({
|
|
title: '',
|
|
text: adminData.languages['delete_active_item'],
|
|
type: "warning",
|
|
showCancelButton: true,
|
|
confirmButtonColor: "#DD6B55",
|
|
cancelButtonText: adminData.languages['cancel'],
|
|
confirmButtonText: adminData.languages['delete'],
|
|
showLoaderOnConfirm: true,
|
|
closeOnConfirm: false
|
|
}, function () {
|
|
var mykey = key ? key : self[self.primaryKey]();
|
|
|
|
self.freezeForm(true);
|
|
|
|
$.ajax({
|
|
url: base_url + self.modelName() + '/' + mykey + '/delete',
|
|
data: {_token: csrf},
|
|
dataType: 'json',
|
|
type: 'POST',
|
|
complete: function () {
|
|
self.freezeForm(false);
|
|
window.admin.resizePage();
|
|
},
|
|
success: function (response) {
|
|
if (response.success) {
|
|
swal({
|
|
title: adminData.languages['deleted'],
|
|
text: "",
|
|
type: "success",
|
|
timer: 1000,
|
|
showConfirmButton: false
|
|
});
|
|
|
|
self.updateRows();
|
|
self.updateSelfRelationships();
|
|
|
|
if (mykey == self[self.primaryKey]()) {
|
|
setTimeout(function () {
|
|
History.pushState({modelName: self.modelName()}, document.title, route + self.modelName());
|
|
$('#sidebar').fadeIn();
|
|
}, 500);
|
|
}
|
|
}
|
|
else
|
|
swal(response.error, "", "error");
|
|
},
|
|
error: function (response) {
|
|
swal(adminData.languages['delete_failed'], "", "error");
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Deletes selected items
|
|
*/
|
|
deleteItems: function () {
|
|
var self = this;
|
|
var selected = [];
|
|
|
|
$('.select-checkbox').each(function (i, el) {
|
|
if ($(el).is(':checked')) {
|
|
selected.push($(el).val());
|
|
}
|
|
});
|
|
|
|
if (!selected.length) {
|
|
swal('', adminData.languages['select_options'], "warning");
|
|
return;
|
|
}
|
|
|
|
swal({
|
|
title: '',
|
|
text: adminData.languages['delete_items'],
|
|
type: "warning",
|
|
showCancelButton: true,
|
|
confirmButtonColor: "#DD6B55",
|
|
cancelButtonText: adminData.languages['cancel'],
|
|
confirmButtonText: adminData.languages['delete'],
|
|
showLoaderOnConfirm: true,
|
|
closeOnConfirm: false
|
|
}, function () {
|
|
var mykey = selected.join(',');
|
|
|
|
self.freezeForm(true);
|
|
|
|
$.ajax({
|
|
url: base_url + self.modelName() + '/batch_delete',
|
|
data: {_token: csrf, ids: mykey},
|
|
dataType: 'json',
|
|
type: 'POST',
|
|
complete: function () {
|
|
self.freezeForm(false);
|
|
window.admin.resizePage();
|
|
},
|
|
success: function (response) {
|
|
if (response.success) {
|
|
swal({
|
|
title: adminData.languages['deleted'],
|
|
text: "",
|
|
type: "success",
|
|
timer: 1000,
|
|
showConfirmButton: false
|
|
});
|
|
|
|
self.updateRows();
|
|
self.updateSelfRelationships();
|
|
|
|
setTimeout(function () {
|
|
History.pushState({modelName: self.modelName()}, document.title, route + self.modelName());
|
|
$('#sidebar').fadeIn();
|
|
$('#select-all').prop('checked', false);
|
|
$('#delete-all').addClass('disabled');
|
|
}, 500);
|
|
}
|
|
else
|
|
swal(response.error, "", "error");
|
|
},
|
|
error: function (response) {
|
|
swal(adminData.languages['delete_failed'], "", "error");
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Callback for clicking an item
|
|
*/
|
|
clickItem: function (id) {
|
|
if (!this.loadingItem() && this.activeItem() !== id && this.actionPermissions.view) {
|
|
History.pushState({
|
|
modelName: this.modelName(),
|
|
id: id
|
|
}, document.title, route + this.modelName() + '/' + id);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Gets the active item in the grid
|
|
*
|
|
* @param int id
|
|
*/
|
|
getItem: function (id) {
|
|
var self = this;
|
|
|
|
self.loadingItem(true);
|
|
|
|
//override the edit fields to the original non-existent model
|
|
adminData.edit_fields = self.originalEditFields;
|
|
self.editFields(window.admin.prepareEditFields());
|
|
|
|
//make sure constraints are only loaded once
|
|
self.holdConstraintsQueue = true;
|
|
|
|
//update all the info to the new item state
|
|
ko.mapping.updateData(self, self.model, self.model);
|
|
self.originalData = {};
|
|
|
|
//scroll to the top of the page
|
|
//$('html, body').animate({scrollTop: 0}, 'fast')
|
|
|
|
//if this is a new item (id is falsy), just overwrite the viewModel with the original data model
|
|
if (!id) {
|
|
self.setUpNewItem();
|
|
return;
|
|
}
|
|
|
|
//freeze the relationship constraint updates
|
|
self.freezeConstraints = true;
|
|
|
|
self.itemLoadingId(id);
|
|
|
|
$.ajax({
|
|
url: base_url + self.modelName() + '/' + id,
|
|
dataType: 'json',
|
|
success: function (data) {
|
|
//if there was an error, kick out
|
|
if (data.success === false && data.errors) {
|
|
alert(data.errors);
|
|
return;
|
|
}
|
|
|
|
if (self.itemLoadingId() !== id) {
|
|
//if there are no currently-loading items, clear the form
|
|
if (self.itemLoadingId() === null) {
|
|
self.loadingItem(false);
|
|
self.clearItem();
|
|
}
|
|
}
|
|
else
|
|
self.setData(data);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Sets the edit form up as a new item
|
|
*/
|
|
setUpNewItem: function () {
|
|
this.itemLoadingId(null);
|
|
this.activeItem(0);
|
|
|
|
//set the last item property which helps manage the animation states
|
|
this.lastItem = 0;
|
|
|
|
var data = {};
|
|
|
|
// 新建时加载 belongs_to_many 或 has_many 的默认值
|
|
$.each(adminData.edit_fields, function (ind, el) {
|
|
if (el.type == 'belongs_to_many' || el.type == 'has_many') {
|
|
if (el.value) {
|
|
data[ind] = el.value;
|
|
}
|
|
}
|
|
});
|
|
ko.mapping.updateData(this, this.model, data);
|
|
|
|
this.loadingItem(false);
|
|
|
|
//run the constraints queue
|
|
window.admin.runConstraintsQueue();
|
|
},
|
|
|
|
/**
|
|
* Overrides the data in the view model
|
|
*
|
|
* @param object data
|
|
* @param
|
|
*/
|
|
setData: function (data) {
|
|
var self = this;
|
|
|
|
//set the active item and update the model data
|
|
self.activeItem(data[self.primaryKey]);
|
|
self.loadingItem(false);
|
|
|
|
//update the edit fields
|
|
adminData.edit_fields = data.administrator_edit_fields;
|
|
self.editFields(window.admin.prepareEditFields());
|
|
|
|
//update the actions and the action permissions
|
|
self.actions(data.administrator_actions);
|
|
self.actionPermissions = data.administrator_action_permissions;
|
|
|
|
//set the original values
|
|
self.originalData = data;
|
|
|
|
//set the new options for relationships
|
|
$.each(adminData.edit_fields, function (ind, el) {
|
|
if (el.relationship && el.autocomplete) {
|
|
self[el.field_name + '_autocomplete'] = data[el.field_name + '_autocomplete'];
|
|
}
|
|
});
|
|
|
|
//set the item link if it exists
|
|
if (data.admin_item_link) {
|
|
self.itemLink(data.admin_item_link);
|
|
}
|
|
|
|
//set the last item property which helps manage the animation states
|
|
self.lastItem = data[self.primaryKey];
|
|
|
|
//fixes an error where the relationships wouldn't load
|
|
setTimeout(function () {
|
|
//first clear the data
|
|
ko.mapping.updateData(self, self.model, self.model);
|
|
|
|
//then update the data
|
|
ko.mapping.updateData(self, self.model, data);
|
|
|
|
//unfreeze the relationship constraint updates
|
|
self.freezeConstraints = false;
|
|
|
|
window.admin.resizePage();
|
|
|
|
//run the constraints queue
|
|
window.admin.runConstraintsQueue();
|
|
}, 50);
|
|
},
|
|
|
|
/**
|
|
* Closes the item edit/create window
|
|
*/
|
|
closeItem: function () {
|
|
History.pushState({modelName: this.modelName()}, document.title, route + this.modelName());
|
|
$('#sidebar').fadeIn();
|
|
},
|
|
|
|
/**
|
|
* Clears the current item
|
|
*/
|
|
clearItem: function () {
|
|
this.freezeForm(false);
|
|
this.statusMessage('');
|
|
this.statusMessageType('');
|
|
this.itemLink(null);
|
|
this.itemLoadingId(null);
|
|
this.activeItem(null);
|
|
this.lastItem = null;
|
|
},
|
|
|
|
/**
|
|
* Opens the create item form
|
|
*/
|
|
addNewItem: function () {
|
|
//$('#users_list').resetSelection();
|
|
this.getItem(0);
|
|
},
|
|
|
|
/**
|
|
* Performs a custom action on an item or the whole model
|
|
*
|
|
* @param bool isItem
|
|
* @param string action
|
|
* @param object messages
|
|
* @param string confirmation
|
|
*/
|
|
customAction: function (isItem, action, messages, confirmation) {
|
|
var self = this,
|
|
data = {_token: csrf, action_name: action},
|
|
url;
|
|
|
|
//if a confirmation string was supplied, flash it in a confirm()
|
|
if (confirmation) {
|
|
if (!confirm(confirmation))
|
|
return false;
|
|
}
|
|
|
|
//if this is an item action (compared to a global model action), set the proper url
|
|
if (isItem) {
|
|
url = base_url + self.modelName() + '/' + self[self.primaryKey]() + '/custom_action';
|
|
self.statusMessage(messages.active).statusMessageType('');
|
|
}
|
|
//otherwise set the url and add the filters
|
|
else {
|
|
url = base_url + self.modelName() + '/custom_action';
|
|
data.sortOptions = self.sortOptions;
|
|
data.filters = self.getFilters();
|
|
data.page = self.pagination.page();
|
|
self.globalStatusMessage(messages.active).globalStatusMessageType('');
|
|
}
|
|
|
|
self.freezeForm(true);
|
|
|
|
$.ajax({
|
|
url: url,
|
|
data: data,
|
|
dataType: 'json',
|
|
type: 'POST',
|
|
complete: function () {
|
|
self.freezeForm(false);
|
|
},
|
|
success: function (response) {
|
|
if (response.success) {
|
|
if (isItem) {
|
|
self.statusMessage(messages.success).statusMessageType('success');
|
|
self.setData(response.data);
|
|
}
|
|
else {
|
|
self.globalStatusMessage(messages.success).globalStatusMessageType('success');
|
|
}
|
|
|
|
// if this is a redirect, redirect the user to the supplied url
|
|
if (response.redirect)
|
|
window.location.href = response.redirect;
|
|
|
|
//if there was a file download initiated, redirect the user to the file download address
|
|
if (response.download)
|
|
self.downloadFile(response.download);
|
|
|
|
self.updateRows();
|
|
}
|
|
else {
|
|
if (isItem)
|
|
self.statusMessage(response.error).statusMessageType('error');
|
|
else
|
|
self.globalStatusMessage(response.error).globalStatusMessageType('error');
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Initiates a file download
|
|
*
|
|
* @param string url
|
|
*/
|
|
downloadFile: function (url) {
|
|
var hiddenIFrameId = 'hiddenDownloader',
|
|
iframe = document.getElementById(hiddenIFrameId);
|
|
|
|
if (iframe === null) {
|
|
iframe = document.createElement('iframe');
|
|
iframe.id = hiddenIFrameId;
|
|
iframe.style.display = 'none';
|
|
document.body.appendChild(iframe);
|
|
}
|
|
|
|
iframe.src = url;
|
|
},
|
|
|
|
/**
|
|
* Updates the rows given the data model's current state. Set sort, filters, and anything else before you call this.
|
|
* Calling this locks the results table.
|
|
*
|
|
* @param object data
|
|
*/
|
|
updateRows: function () {
|
|
var self = this,
|
|
id = ++self.rowLoadingId,
|
|
data = {
|
|
_token: csrf,
|
|
sortOptions: self.sortOptions,
|
|
filters: self.getFilters(),
|
|
page: self.pagination.page(),
|
|
// hack by @Monkey: for paging logic
|
|
filter_by: self.filter_by,
|
|
filter_by_id: self.filter_by_id
|
|
};
|
|
|
|
//if the page hasn't been initialized yet, don't update the rows
|
|
if (!this.initialized())
|
|
return;
|
|
|
|
//if we're on page 0 (i.e. there is currently no result set, set the page to 1)
|
|
if (!data.page)
|
|
data.page = 1;
|
|
|
|
//set loadingRows to true so that the loading box comes up
|
|
self.loadingRows(true);
|
|
|
|
$.ajax({
|
|
url: base_url + self.modelName() + '/results',
|
|
type: 'POST',
|
|
dataType: 'json',
|
|
data: data,
|
|
success: function (response) {
|
|
//if the row loading id has changed, that means it's old...so don't use this data
|
|
if (self.rowLoadingId !== id) {
|
|
return;
|
|
}
|
|
|
|
//otherwise the rows aren't loading anymore and we can replace the data
|
|
self.pagination.page(response.last ? response.page : response.last);
|
|
self.pagination.last(response.last);
|
|
self.pagination.total(response.total);
|
|
self.rows(response.results);
|
|
self.loadingRows(false);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Updates the sort options when a column header is clicked
|
|
*
|
|
* @param string field
|
|
*/
|
|
setSortOptions: function (field) {
|
|
//check if the field is a valid column
|
|
var found = false;
|
|
|
|
//iterate over the columns to check if it's a valid sort_field or field
|
|
$.each(this.columns(), function (i, col) {
|
|
if (field === col.sort_field || field === col.column_name) {
|
|
found = true;
|
|
return false;
|
|
}
|
|
})
|
|
|
|
if (!found)
|
|
return false;
|
|
|
|
//the direction depends on the field
|
|
if (field == this.sortOptions.field())
|
|
//reverse the direction
|
|
this.sortOptions.direction((this.sortOptions.direction() == 'asc') ? 'desc' : 'asc');
|
|
else
|
|
//set the direction to asc
|
|
this.sortOptions.direction('asc');
|
|
|
|
//update the field
|
|
this.sortOptions.field(field);
|
|
|
|
//update the rows
|
|
this.updateRows();
|
|
},
|
|
|
|
/**
|
|
* Goes to the specified page
|
|
*
|
|
* @param string|int page
|
|
*/
|
|
page: function (page) {
|
|
var currPage = parseInt(this.pagination.page()),
|
|
newPage = 1,
|
|
lastPage = parseInt(this.pagination.last());
|
|
|
|
//if the value is 'prev' or 'next', increment or decrement
|
|
if (page === 'prev') {
|
|
if (currPage > 1) {
|
|
newPage = currPage - 1;
|
|
}
|
|
}
|
|
else if (page === 'next') {
|
|
if (currPage < lastPage) {
|
|
newPage = currPage + 1;
|
|
}
|
|
else {
|
|
newPage = lastPage;
|
|
}
|
|
}
|
|
else if (!isNaN(parseInt(page))) {
|
|
//set the page to the supplied value
|
|
if (page > lastPage) {
|
|
newPage = lastPage;
|
|
}
|
|
else {
|
|
newPage = page;
|
|
}
|
|
}
|
|
|
|
this.pagination.page(newPage);
|
|
|
|
//update the rows
|
|
this.updateRows();
|
|
},
|
|
|
|
/**
|
|
* Updates the rows per page for this model when the item is changed
|
|
*
|
|
* @param int
|
|
*/
|
|
updateRowsPerPage: function (rows) {
|
|
var self = this;
|
|
|
|
$.ajax({
|
|
url: rows_per_page_url,
|
|
data: {_token: csrf, rows: rows},
|
|
dataType: 'json',
|
|
type: 'POST',
|
|
complete: function () {
|
|
self.updateRows();
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Gets a minimized filters array that can be sent to the server
|
|
*/
|
|
getFilters: function () {
|
|
var filters = [],
|
|
observables = ['value', 'min_value', 'max_value'];
|
|
|
|
$.each(window.admin.filtersViewModel.filters, function (ind, el) {
|
|
var filter = {
|
|
field_name: el.field_name,
|
|
type: el.type,
|
|
value: el.value() ? el.value() : null,
|
|
};
|
|
|
|
//iterate over the observables to see if we should include them
|
|
$(observables).each(function (i, obs) {
|
|
if (this in el) {
|
|
filter[this] = el[this]() ? el[this]() : null;
|
|
|
|
if (obs === 'value' && filter[this] && el.type === 'belongs_to_many' && typeof filter[this] === 'string') {
|
|
filter.value = filter.value.split(',');
|
|
}
|
|
}
|
|
});
|
|
|
|
//push this filter onto the filters array
|
|
filters.push(filter);
|
|
});
|
|
|
|
return filters;
|
|
},
|
|
|
|
/**
|
|
* Determines if the provided field is dirty
|
|
*
|
|
* @param string
|
|
*
|
|
* @return bool
|
|
*/
|
|
fieldIsDirty: function (field) {
|
|
return this.originalData[field] != this[field]();
|
|
},
|
|
|
|
/**
|
|
* Updates any self-relationships
|
|
*/
|
|
updateSelfRelationships: function () {
|
|
var self = this;
|
|
|
|
//first we will iterate over the filters and update them if any exist
|
|
$.each(window.admin.filtersViewModel.filters, function (ind, filter) {
|
|
var fieldIndex = ind,
|
|
fieldName = filter.field_name;
|
|
|
|
if ((!filter.constraints || !filter.constraints.length) && filter.self_relationship) {
|
|
window.admin.filtersViewModel.filters[fieldIndex].loadingOptions(true);
|
|
|
|
$.ajax({
|
|
url: base_url + self.modelName() + '/update_options',
|
|
type: 'POST',
|
|
dataType: 'json',
|
|
data: {
|
|
fields: [{
|
|
type: 'filter',
|
|
field: fieldName,
|
|
selectedItems: filter.value()
|
|
}]
|
|
},
|
|
complete: function () {
|
|
window.admin.filtersViewModel.filters[fieldIndex].loadingOptions(false);
|
|
},
|
|
success: function (response) {
|
|
//update the options
|
|
window.admin.filtersViewModel.listOptions[fieldName](response[fieldName]);
|
|
}
|
|
});
|
|
|
|
}
|
|
});
|
|
|
|
//then we'll update the edit fields
|
|
$.each(self.editFields(), function (ind, field) {
|
|
var fieldName = field.field_name;
|
|
|
|
//if there are no constraints for this field and if it is a self-relationship, update the options
|
|
if ((!field.constraints || !field.constraints.length) && field.self_relationship) {
|
|
field.loadingOptions(true);
|
|
|
|
$.ajax({
|
|
url: base_url + self.modelName() + '/update_options',
|
|
type: 'POST',
|
|
dataType: 'json',
|
|
data: {
|
|
fields: [{
|
|
type: 'edit',
|
|
field: fieldName,
|
|
selectedItems: self[fieldName]()
|
|
}]
|
|
},
|
|
complete: function () {
|
|
field.loadingOptions(false);
|
|
},
|
|
success: function (response) {
|
|
//update the options
|
|
self.listOptions[fieldName] = response[fieldName];
|
|
}
|
|
});
|
|
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
|
|
//methods
|
|
|
|
/**
|
|
* Init method
|
|
*/
|
|
init: function () {
|
|
var self = this;
|
|
|
|
//set up the basic pieces of data
|
|
this.viewModel.model = adminData.data_model;
|
|
this.$container = $('#admin_content');
|
|
|
|
var viewModel = ko.mapping.fromJS(this.viewModel.model);
|
|
|
|
$.extend(this.viewModel, viewModel);
|
|
|
|
this.viewModel.rows(adminData.rows.results);
|
|
this.viewModel.pagination.page(adminData.rows.page);
|
|
this.viewModel.pagination.last(adminData.rows.last);
|
|
this.viewModel.pagination.total(adminData.rows.total);
|
|
this.viewModel.sortOptions.field(adminData.sortOptions.field);
|
|
this.viewModel.sortOptions.direction(adminData.sortOptions.direction);
|
|
this.viewModel.columns(this.prepareColumns());
|
|
this.viewModel.modelName(adminData.model_name);
|
|
this.viewModel.modelTitle(adminData.model_title);
|
|
this.viewModel.subTitle(adminData.sub_title);
|
|
this.viewModel.modelSingle(adminData.model_single);
|
|
this.viewModel.expandWidth(adminData.expand_width);
|
|
this.viewModel.rowsPerPage(adminData.rows_per_page);
|
|
this.viewModel.primaryKey = adminData.primary_key;
|
|
this.viewModel.actions(adminData.actions);
|
|
this.viewModel.globalActions(adminData.global_actions);
|
|
this.viewModel.actionPermissions = adminData.action_permissions;
|
|
this.viewModel.languages = adminData.languages;
|
|
// hack by @Monkey: for paging logic
|
|
this.viewModel.filter_by = adminData.filter_by;
|
|
this.viewModel.filter_by_id = adminData.filter_by_id;
|
|
|
|
//set up the rowsPerPageOptions
|
|
var perPageArr = [20, 50, 100, 200, 500, 1000, 2000, 5000, 8000, 10000];
|
|
for (var i = 0; i < perPageArr.length; i++) {
|
|
this.viewModel.rowsPerPageOptions.push({id: perPageArr[i], text: perPageArr[i] + ''});
|
|
}
|
|
|
|
//now that we have most of our data, we can set up the computed values
|
|
this.initComputed();
|
|
|
|
//prepare the filters
|
|
this.filtersViewModel.filters = this.prepareFilters();
|
|
|
|
//prepare the edit fields
|
|
this.viewModel.originalEditFields = adminData.edit_fields;
|
|
this.viewModel.editFields(this.prepareEditFields());
|
|
|
|
//set up the relationships
|
|
this.initRelationships();
|
|
|
|
//set up the KO bindings
|
|
ko.applyBindings(this.viewModel, $('#content')[0]);
|
|
ko.applyBindings(this.filtersViewModel, $('#filters_sidebar_section')[0]);
|
|
|
|
//set up pushstate history
|
|
this.initHistory();
|
|
|
|
//set up the subscriptions
|
|
this.initSubscriptions();
|
|
|
|
//set up the events
|
|
this.initEvents();
|
|
|
|
//run an initial page resize
|
|
this.resizePage();
|
|
|
|
//finally run a timer to overcome bugs with select2
|
|
setTimeout(function () {
|
|
self.viewModel.initialized(true);
|
|
|
|
}, 1000);
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Prepare the filters
|
|
*
|
|
* @return array with value observables
|
|
*/
|
|
prepareFilters: function () {
|
|
var filters = [];
|
|
|
|
$.each(adminData.filters, function (ind, filter) {
|
|
var observables = ['value', 'min_value', 'max_value'];
|
|
|
|
//iterate over the desired observables and check if they're there. if so, assign them an observable slot
|
|
$.each(observables, function (i, obs) {
|
|
if (obs in filter) {
|
|
filter[obs] = ko.observable(filter[obs]);
|
|
}
|
|
});
|
|
|
|
//if this is a relationship field, we want to set up the loading options observable
|
|
if (filter.relationship) {
|
|
filter.loadingOptions = ko.observable(false);
|
|
}
|
|
|
|
filter.field_id = 'filter_field_' + filter.field_name;
|
|
|
|
filters.push(filter);
|
|
});
|
|
|
|
return filters;
|
|
},
|
|
|
|
/**
|
|
* Prepare the edit fields
|
|
*
|
|
* @return object with loadingOptions observables
|
|
*/
|
|
prepareEditFields: function () {
|
|
var self = this,
|
|
fields = [];
|
|
|
|
$.each(adminData.edit_fields, function (ind, field) {
|
|
//if this is a relationship field, set up the loadingOptions observable
|
|
if (field.relationship) {
|
|
field.loadingOptions = ko.observable(false);
|
|
field.constraintLoading = ko.observable(false);
|
|
}
|
|
|
|
//if this is an image field, set the upload params
|
|
if (field.type === 'image' || field.type === 'file') {
|
|
field.uploading = ko.observable(false);
|
|
field.upload_percentage = ko.observable(0);
|
|
}
|
|
|
|
//add the id field
|
|
field.field_id = 'edit_field_' + ind;
|
|
|
|
fields.push(field);
|
|
});
|
|
|
|
return fields;
|
|
},
|
|
|
|
/**
|
|
* Sets up the column model with various observable values
|
|
*
|
|
* @return array
|
|
*/
|
|
prepareColumns: function () {
|
|
var self = this,
|
|
columns = [];
|
|
|
|
$.each(adminData.column_model, function (ind, column) {
|
|
column.visible = ko.observable(column.visible);
|
|
columns.push(column);
|
|
});
|
|
|
|
return columns;
|
|
},
|
|
|
|
/**
|
|
* Set up the relationship items
|
|
*/
|
|
initRelationships: function () {
|
|
var self = this;
|
|
|
|
//set up the filters
|
|
$.each(adminData.filters, function (ind, el) {
|
|
if (el.relationship)
|
|
self.filtersViewModel.listOptions[ind] = ko.observableArray(el.options);
|
|
});
|
|
|
|
//set up the edit fields
|
|
$.each(adminData.edit_fields, function (ind, el) {
|
|
if (el.relationship)
|
|
self.viewModel.listOptions[ind] = el.options;
|
|
|
|
// add any loaded option to the autocomplete array
|
|
if (el.autocomplete) {
|
|
if (!(el.field_name + '_autocomplete' in self.viewModel))
|
|
self.viewModel[el.field_name + '_autocomplete'] = [];
|
|
$.each(el.options, function (x, option) {
|
|
self.viewModel[el.field_name + '_autocomplete'][option.id] = option;
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Inits the KO subscriptions
|
|
*/
|
|
initSubscriptions: function () {
|
|
var self = this,
|
|
runFilter = function (val) {
|
|
self.viewModel.updateRows();
|
|
};
|
|
|
|
//iterate over filters
|
|
$.each(self.filtersViewModel.filters, function (ind, filter) {
|
|
//subscribe to the value field
|
|
self.filtersViewModel.filters[ind].value.subscribe(function (val) {
|
|
//if this is an id field, make sure it's an integer
|
|
if (self.filtersViewModel.filters[ind].type === 'key') {
|
|
var intVal = isNaN(parseInt(val)) ? '' : parseInt(val);
|
|
|
|
self.filtersViewModel.filters[ind].value(intVal);
|
|
}
|
|
|
|
//update the rows now that we've got new filters
|
|
self.viewModel.updateRows();
|
|
});
|
|
|
|
//check if there's a min and max value. if so, subscribe to those as well
|
|
if ('min_value' in filter) {
|
|
self.filtersViewModel.filters[ind].min_value.subscribe(runFilter);
|
|
}
|
|
if ('max_value' in filter) {
|
|
self.filtersViewModel.filters[ind].max_value.subscribe(runFilter);
|
|
}
|
|
});
|
|
|
|
//iterate over the edit fields
|
|
$.each(self.viewModel.editFields(), function (ind, field) {
|
|
//if there are constraints to maintain, set up the subscriptions
|
|
if (field.constraints && self.getObjectSize(field.constraints)) {
|
|
self.establishFieldConstraints(field);
|
|
}
|
|
});
|
|
|
|
//subscribe to page change
|
|
self.viewModel.pagination.page.subscribe(function (val) {
|
|
self.viewModel.page(val);
|
|
});
|
|
|
|
//subscribe to rows per page change
|
|
self.viewModel.rowsPerPage.subscribe(function (val) {
|
|
self.viewModel.updateRowsPerPage(parseInt(val));
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Establish constraints
|
|
*
|
|
* @param object field
|
|
*/
|
|
establishFieldConstraints: function (field) {
|
|
var self = this;
|
|
|
|
//we want to subscribe to changes on the OTHER fields since that's what defines changes to this one
|
|
$.each(field.constraints, function (key, relationshipName) {
|
|
var fieldName = field.field_name,
|
|
f = field,
|
|
constraintsLength = self.getFieldConstraintsLength(key);
|
|
|
|
self.viewModel[key].subscribe(function (val) {
|
|
if (self.viewModel.freezeConstraints || f.loadingOptions())
|
|
return;
|
|
|
|
//if this key hasn't been set up yet, set it
|
|
if (!self.viewModel.constraintsQueue[key])
|
|
self.viewModel.constraintsQueue[key] = {};
|
|
|
|
//add the constraint to the queue
|
|
self.viewModel.constraintsQueue[key][fieldName] = f;
|
|
|
|
var currentQueueLength = Object.keys(self.viewModel.constraintsQueue[key]).length;
|
|
|
|
if (!self.viewModel.holdConstraintsQueue && (currentQueueLength === constraintsLength))
|
|
self.runConstraintsQueue();
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Sets the constrainer's constraintLoading field to true
|
|
*
|
|
* @param string key
|
|
*
|
|
* @return int
|
|
*/
|
|
getFieldConstraintsLength: function (key) {
|
|
var length = 0;
|
|
|
|
//iterate over the edit fields until we find our match
|
|
$.each(this.viewModel.editFields(), function (ind, field) {
|
|
if (field.constraints && field.constraints[key]) {
|
|
length++;
|
|
}
|
|
});
|
|
|
|
return length;
|
|
},
|
|
|
|
/**
|
|
* Sets the constrainer's constraintLoading field to true
|
|
*
|
|
* @param string key
|
|
* @param bool freeze
|
|
*/
|
|
setConstrainerFreeze: function (key, freeze) {
|
|
//iterate over the edit fields until we find our match
|
|
$.each(this.viewModel.editFields(), function (ind, field) {
|
|
if (field.field_name === key) {
|
|
field.constraintLoading(freeze);
|
|
return false;
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Sets a field's loadingOptions
|
|
*
|
|
* @param string fieldName
|
|
* @param bool type
|
|
*/
|
|
setFieldLoadingOptions: function (fieldName, type) {
|
|
//iterate over the edit fields until we find our match
|
|
$.each(this.viewModel.editFields(), function (ind, field) {
|
|
if (field.field_name === fieldName) {
|
|
field.loadingOptions(type);
|
|
return false;
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Runs the constraints queue
|
|
*/
|
|
runConstraintsQueue: function () {
|
|
var self = this,
|
|
fields = self.buildConstraintsFromQueue();
|
|
|
|
//if there are no fields, exit out
|
|
if (!fields.length)
|
|
return;
|
|
|
|
//freeze the actions
|
|
self.viewModel.freezeActions(true);
|
|
|
|
$.ajax({
|
|
url: base_url + self.viewModel.modelName() + '/update_options',
|
|
type: 'POST',
|
|
dataType: 'json',
|
|
data: {
|
|
fields: fields
|
|
},
|
|
complete: function () {
|
|
self.viewModel.freezeActions(false);
|
|
|
|
$.each(self.viewModel.constraintsQueue, function (key, fieldConstraints) {
|
|
$.each(fieldConstraints, function (fieldName, field) {
|
|
self.setFieldLoadingOptions(fieldName, false);
|
|
self.setConstrainerFreeze(key, false);
|
|
});
|
|
});
|
|
|
|
//clear the constraints queue
|
|
self.viewModel.constraintsQueue = {};
|
|
self.viewModel.holdConstraintsQueue = false;
|
|
},
|
|
success: function (response) {
|
|
//iterate over the results and put them in the autocomplete array
|
|
$.each(response, function (fieldName, el) {
|
|
var data = {};
|
|
|
|
$.each(el, function (i, e) {
|
|
data[e.id] = e;
|
|
});
|
|
|
|
self.viewModel[fieldName + '_autocomplete'] = data;
|
|
|
|
//update the options
|
|
self.viewModel.listOptions[fieldName] = el;
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Prepares the constraints for the queue job
|
|
*/
|
|
buildConstraintsFromQueue: function () {
|
|
var self = this,
|
|
allConstraints = [];
|
|
|
|
$.each(self.viewModel.constraintsQueue, function (key, fieldConstraints) {
|
|
$.each(fieldConstraints, function (fieldName, field) {
|
|
var constraints = {};
|
|
|
|
//set the field to loading and freeze the constrainer
|
|
self.setFieldLoadingOptions(fieldName, true);
|
|
self.setConstrainerFreeze(key, true);
|
|
|
|
//iterate over this field's constraints
|
|
$.each(field.constraints, function (key, relationshipName) {
|
|
constraints[key] = self.viewModel[key]();
|
|
});
|
|
|
|
allConstraints.push({
|
|
constraints: constraints,
|
|
type: 'edit',
|
|
field: fieldName,
|
|
selectedItems: self.viewModel[fieldName]()
|
|
});
|
|
});
|
|
});
|
|
|
|
return allConstraints;
|
|
},
|
|
|
|
/**
|
|
* Inits the page events
|
|
*/
|
|
initEvents: function () {
|
|
var self = this;
|
|
|
|
//clicking the new item button
|
|
$('#content').on('click', 'div.results_header a.new_item', function (e) {
|
|
e.preventDefault();
|
|
History.pushState({
|
|
modelName: self.viewModel.modelName(),
|
|
id: 0
|
|
}, document.title, route + self.viewModel.modelName() + '/new');
|
|
});
|
|
|
|
//resizing the window
|
|
$(window).resize(self.resizePage);
|
|
|
|
//mousedowning or keypressing anywhere should resize the page as well
|
|
$('body').on('mouseup keypress', self.resizePage);
|
|
|
|
//set up the history event callback
|
|
History.Adapter.bind(window, 'statechange', function () {
|
|
var state = History.getState();
|
|
|
|
//if the ignore key is true, or if this is the inital state, exit out.
|
|
if (state.data.ignore || (state.data.init && !self.historyStarted))
|
|
return;
|
|
|
|
|
|
//if the model name is present
|
|
if ('modelName' in state.data)
|
|
//if that model name isn't the current model name, we are updating the model
|
|
if (state.data.modelName !== self.viewModel.modelName())
|
|
//get the new model
|
|
self.viewModel.getNewModel(state.data);
|
|
|
|
//if the state data has an id field and if it's not the active item
|
|
if ('id' in state.data) {
|
|
//get the new item (this includes when state.data.id === 0, which means it should be a new item)
|
|
if (state.data.id !== self.viewModel.activeItem())
|
|
self.viewModel.getItem(state.data.id);
|
|
}
|
|
else {
|
|
//otherwise, assume that the user wants to be taken back to the results page. close the form
|
|
self.viewModel.clearItem();
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Sets up the push state's initial state
|
|
*/
|
|
initHistory: function () {
|
|
var historyData = {
|
|
modelName: this.viewModel.modelName(),
|
|
init: true
|
|
},
|
|
uri = route + this.viewModel.modelName();
|
|
|
|
//if the admin data had an id supplied, it means this is either the edit page or the new item page
|
|
if ('id' in adminData) {
|
|
//if the view model hasn't been set up yet, wait for it to be set up
|
|
var timer = setInterval(function () {
|
|
if (window.admin) {
|
|
window.admin.viewModel.getItem(adminData.id);
|
|
historyData.id = adminData.id;
|
|
uri += '/' + (historyData.id ? historyData.id : 'new');
|
|
|
|
//now call the same to trigger the statechange event
|
|
History.pushState(historyData, document.title, uri);
|
|
|
|
clearInterval(timer);
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
this.historyStarted = true;
|
|
},
|
|
|
|
/**
|
|
* Initializes the computed observables
|
|
*/
|
|
initComputed: function () {
|
|
//pagination information
|
|
this.viewModel.pagination.isFirst = ko.computed(function () {
|
|
return this.pagination.page() == 1;
|
|
}, this.viewModel);
|
|
|
|
this.viewModel.pagination.isLast = ko.computed(function () {
|
|
return this.pagination.page() == this.pagination.last();
|
|
}, this.viewModel);
|
|
|
|
},
|
|
|
|
/**
|
|
* Helper to get an object's size
|
|
*
|
|
* @param object
|
|
*
|
|
* @return int
|
|
*/
|
|
getObjectSize: function (obj) {
|
|
var size = 0, key;
|
|
|
|
for (key in obj) {
|
|
if (obj.hasOwnProperty(key)) size++;
|
|
}
|
|
|
|
return size;
|
|
},
|
|
|
|
/**
|
|
* Handles a window resize to make sure the admin area is always
|
|
*/
|
|
resizePage: function () {
|
|
setTimeout(function () {
|
|
var winHeight = $(window).height(),
|
|
itemEditHeight = $('div.item_edit').outerHeight() + 50,
|
|
usedHeight = winHeight > itemEditHeight ? winHeight - 45 : itemEditHeight,
|
|
size = window.getComputedStyle(document.body, ':after').getPropertyValue('content');
|
|
|
|
//resize the page height
|
|
$('#admin_page').css({minHeight: usedHeight + 45});
|
|
|
|
//resize or scroll the data table
|
|
if (window.admin) {
|
|
if (!window.admin.dataTableScrollable)
|
|
window.admin.resizeDataTable();
|
|
else
|
|
window.admin.scrollDataTable();
|
|
}
|
|
|
|
|
|
// Popover with html
|
|
$('.popover-with-html').popover({
|
|
html: true,
|
|
// trigger : 'click hover',
|
|
trigger: 'manual',
|
|
container: 'body',
|
|
placement: 'top',
|
|
delay: {show: 50, hide: 400},
|
|
content: function () {
|
|
return $(this).attr('hint');
|
|
}
|
|
}).on("mouseenter", function () {
|
|
var _this = this;
|
|
$(this).popover("show");
|
|
$(".popover").on("mouseleave", function () {
|
|
$(_this).popover('hide');
|
|
});
|
|
}).on("mouseleave", function () {
|
|
var _this = this;
|
|
setTimeout(function () {
|
|
if (!$(".popover:hover").length) {
|
|
$(_this).popover("hide");
|
|
}
|
|
}, 400);
|
|
});
|
|
|
|
}, 50);
|
|
},
|
|
|
|
/**
|
|
* Allows to scroll wide data tables (alternative to resizeDataTable)
|
|
*/
|
|
scrollDataTable: function () {
|
|
if (!self.$tableContainer) {
|
|
self.$tableContainer = $('div.table_container');
|
|
self.$dataTable = self.$tableContainer.find('table.results');
|
|
}
|
|
|
|
// exit if table is already wrapped
|
|
if (self.$dataTable.parent().hasClass('table_scrollable')) return true;
|
|
|
|
// wrap table within div.table_scrollable
|
|
self.$dataTable.wrap('<div class="table_scrollable"></div>')
|
|
},
|
|
|
|
/**
|
|
* Hides columns until the table container is at least as wide as the data table
|
|
*/
|
|
resizeDataTable: function () {
|
|
var self = window.admin,
|
|
winWidth = $(window).width();
|
|
|
|
if (!self.$tableContainer) {
|
|
self.$tableContainer = $('div.table_container');
|
|
self.$dataTable = self.$tableContainer.find('table.results');
|
|
}
|
|
|
|
//grab the columns
|
|
var columns = self.viewModel.columns();
|
|
|
|
//iterate over the column hide points to see if we should unhide any of them
|
|
$.each(self.columnHidePoints, function (i, el) {
|
|
if (el < winWidth)
|
|
columns[i].visible(true);
|
|
});
|
|
|
|
//walk backwards over the columns to determine which ones to hide
|
|
for (var i = columns.length - 1; i >= 2; i--) {
|
|
//if the datatable is visible and the table is large than its container
|
|
if (columns.length >= 2 && self.$dataTable.is(':visible') && (self.$tableContainer.width() < self.$dataTable.width())) {
|
|
//we don't want to hide all the columns
|
|
if (i <= 1)
|
|
return;
|
|
if (columns[i].visible()) {
|
|
columns[i].visible(false);
|
|
self.columnHidePoints[i] = winWidth;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
//set up the admin instance
|
|
$(function () {
|
|
if ($('#admin_page').length) {
|
|
window.admin = new admin();
|
|
}
|
|
|
|
// 二维码
|
|
var qrcode = new QRCode(document.getElementById('qrcode-img'), {
|
|
text: 'http://tianyinzaixian.com',
|
|
width: 320,
|
|
height: 320
|
|
});
|
|
|
|
// $('#qrcode-img').attr('title', '')
|
|
$(document).on('click', '.get-qrcode-btn', function (e) {
|
|
e.preventDefault();
|
|
|
|
// 重新生成二维码
|
|
qrcode.clear(); // clear the code.
|
|
qrcode.makeCode($(this).attr('data-link')); // make another code.
|
|
|
|
$('#getQrcode').modal('show');
|
|
|
|
});
|
|
|
|
// select all items
|
|
$('#select-all').on('click', function () {
|
|
var checked = false;
|
|
|
|
if ($(this).is(':checked')) {
|
|
$('.select-checkbox').prop('checked', true);
|
|
checked = true;
|
|
} else {
|
|
$('.select-checkbox').prop('checked', false);
|
|
}
|
|
|
|
if (checked && $('.select-checkbox').length) {
|
|
$('#delete-all').removeClass('disabled');
|
|
} else {
|
|
$('#delete-all').addClass('disabled');
|
|
}
|
|
});
|
|
|
|
// disable delete-all btn
|
|
$('.select-checkbox').on('click', function () {
|
|
var selected = 0;
|
|
|
|
$('.select-checkbox').each(function (i, el) {
|
|
if ($(el).is(':checked')) {
|
|
selected++;
|
|
}
|
|
});
|
|
|
|
if (selected > 0) {
|
|
$('#delete-all').removeClass('disabled');
|
|
} else {
|
|
$('#delete-all').addClass('disabled');
|
|
}
|
|
});
|
|
|
|
$('[data-toggle="tooltip"]').tooltip();
|
|
});
|
|
})(jQuery);
|
|
|
|
(function ($) {
|
|
var admin = function () {
|
|
return this.init();
|
|
};
|
|
|
|
//setting up csrf token
|
|
$.ajaxSetup({
|
|
headers: {
|
|
'X-CSRF-TOKEN': window.csrf
|
|
}
|
|
});
|
|
|
|
admin.prototype = {
|
|
|
|
//properties
|
|
|
|
/*
|
|
* Main admin container
|
|
*
|
|
* @type jQuery object
|
|
*/
|
|
$container: null,
|
|
|
|
|
|
/*
|
|
* KO viewModel
|
|
*/
|
|
viewModel: {
|
|
|
|
|
|
/* The settings name
|
|
* string
|
|
*/
|
|
settingsName: ko.observable(''),
|
|
|
|
/* The settings title
|
|
* string
|
|
*/
|
|
settingsTitle: ko.observable(''),
|
|
|
|
/* The model edit fields
|
|
* array
|
|
*/
|
|
editFields: ko.observableArray(),
|
|
|
|
/* If this is set to true, the form becomes uneditable
|
|
* bool
|
|
*/
|
|
freezeForm: ko.observable(false),
|
|
|
|
/* If this is set to true, the action buttons on the form cannot be accessed
|
|
* bool
|
|
*/
|
|
freezeActions: ko.observable(false),
|
|
|
|
/* If custom actions are supplied, they are stored here
|
|
* array
|
|
*/
|
|
actions: ko.observableArray(),
|
|
|
|
/* The languages array holds text for the current language
|
|
* object
|
|
*/
|
|
languages: {},
|
|
|
|
/* The status message and the type ('', 'success', 'error')
|
|
* strings
|
|
*/
|
|
statusMessage: ko.observable(''),
|
|
statusMessageType: ko.observable(''),
|
|
|
|
/**
|
|
* Saves the item with the current settings
|
|
*/
|
|
save: function () {
|
|
var self = this,
|
|
saveData = ko.mapping.toJS(self);
|
|
|
|
saveData._token = csrf;
|
|
|
|
self.statusMessage(self.languages['saving']).statusMessageType('');
|
|
self.freezeForm(true);
|
|
|
|
$.ajax({
|
|
url: save_url,
|
|
data: saveData,
|
|
dataType: 'json',
|
|
type: 'POST',
|
|
complete: function () {
|
|
self.freezeForm(false);
|
|
},
|
|
success: function (response) {
|
|
if (response.success) {
|
|
self.statusMessage(self.languages['saved']).statusMessageType('success');
|
|
|
|
//update the model
|
|
self.updateData(response.data);
|
|
|
|
//update the custom actions
|
|
self.actions(response.actions);
|
|
}
|
|
else
|
|
self.statusMessage(response.errors).statusMessageType('error');
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Performs a custom action
|
|
*
|
|
* @param string action
|
|
* @param object messages
|
|
* @param string confirmation
|
|
*/
|
|
customAction: function (action, messages, confirmation) {
|
|
var self = this;
|
|
|
|
//if a confirmation string was supplied, flash it in a confirm()
|
|
if (confirmation) {
|
|
if (!confirm(confirmation))
|
|
return false;
|
|
}
|
|
|
|
self.statusMessage(messages.active).statusMessageType('');
|
|
self.freezeForm(true);
|
|
|
|
$.ajax({
|
|
url: custom_action_url,
|
|
data: {_token: csrf, action_name: action},
|
|
dataType: 'json',
|
|
type: 'POST',
|
|
complete: function () {
|
|
self.freezeForm(false);
|
|
},
|
|
success: function (response) {
|
|
if (response.success) {
|
|
self.statusMessage(messages.success).statusMessageType('success');
|
|
|
|
//update the custom actions
|
|
self.actions(response.actions);
|
|
|
|
// if this is a redirect, redirect the user to the supplied url
|
|
if (response.redirect)
|
|
window.location.href = response.redirect;
|
|
|
|
//if there was a file download initiated, redirect the user to the file download address
|
|
if (response.download)
|
|
self.downloadFile(response.download);
|
|
}
|
|
else
|
|
self.statusMessage(response.error).statusMessageType('error');
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Initiates a file download
|
|
*
|
|
* @param string url
|
|
*/
|
|
downloadFile: function (url) {
|
|
var hiddenIFrameId = 'hiddenDownloader',
|
|
iframe = document.getElementById(hiddenIFrameId);
|
|
|
|
if (iframe === null) {
|
|
iframe = document.createElement('iframe');
|
|
iframe.id = hiddenIFrameId;
|
|
iframe.style.display = 'none';
|
|
document.body.appendChild(iframe);
|
|
}
|
|
|
|
iframe.src = url;
|
|
},
|
|
|
|
/**
|
|
* Updates the view model data
|
|
*
|
|
* @param array data
|
|
*/
|
|
updateData: function (data) {
|
|
var self = this;
|
|
|
|
//iterate over the data and find the associated observable
|
|
$.each(data, function (i, el) {
|
|
self[i](el);
|
|
});
|
|
}
|
|
|
|
},
|
|
|
|
|
|
//methods
|
|
|
|
/**
|
|
* Init method
|
|
*/
|
|
init: function () {
|
|
//set up the basic pieces of data
|
|
this.$container = $('#admin_content');
|
|
|
|
var viewModel = ko.mapping.fromJS(adminData.data);
|
|
|
|
$.extend(this.viewModel, viewModel);
|
|
|
|
this.viewModel.settingsName(adminData.name);
|
|
this.viewModel.settingsTitle(adminData.title);
|
|
this.viewModel.actions(adminData.actions);
|
|
this.viewModel.languages = adminData.languages;
|
|
|
|
//now that we have most of our data, we can set up the computed values
|
|
this.initComputed();
|
|
|
|
//prepare the edit fields
|
|
this.viewModel.editFields = this.prepareEditFields();
|
|
|
|
//set up the KO bindings
|
|
ko.applyBindings(this.viewModel, $('#main_content')[0]);
|
|
|
|
//set up the subscriptions
|
|
this.initSubscriptions();
|
|
|
|
//set up the events
|
|
this.initEvents();
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Prepare the edit fields
|
|
*
|
|
* @return object with loadingOptions observables
|
|
*/
|
|
prepareEditFields: function () {
|
|
var self = this,
|
|
fields = [];
|
|
|
|
$.each(adminData.edit_fields, function (ind, field) {
|
|
//if this is an image field, set the upload params
|
|
if (field.type === 'image' || field.type === 'file') {
|
|
field.uploading = ko.observable(false);
|
|
field.upload_percentage = ko.observable(0);
|
|
}
|
|
|
|
//add the id field
|
|
field.field_id = 'edit_field_' + ind;
|
|
|
|
fields.push(field);
|
|
});
|
|
|
|
return fields;
|
|
},
|
|
|
|
/**
|
|
* Inits the KO subscriptions
|
|
*/
|
|
initSubscriptions: function () {
|
|
var self = this;
|
|
|
|
},
|
|
|
|
/**
|
|
* Inits the page events
|
|
*/
|
|
initEvents: function () {
|
|
var self = this;
|
|
|
|
},
|
|
|
|
/**
|
|
* Initializes the computed observables
|
|
*/
|
|
initComputed: function () {
|
|
|
|
},
|
|
|
|
/**
|
|
* Handles a window resize
|
|
*/
|
|
resizePage: function () {
|
|
|
|
}
|
|
};
|
|
|
|
|
|
//set up the admin instance
|
|
$(function () {
|
|
if ($('#settings_page').length)
|
|
window.admin = new admin();
|
|
});
|
|
})(jQuery);
|
|
(function ($) {
|
|
var $menu, $mobileMenu, $menuButton, $filterButton, $content;
|
|
|
|
//dom ready
|
|
$(function () {
|
|
$menu = $('ul#menu, ul#lang_menu');
|
|
$mobileMenu = $('#mobile_menu_wrapper');
|
|
$menuButton = $('a#menu_button');
|
|
$filterButton = $('a#filter_button');
|
|
$filters = $('#sidebar');
|
|
$content = $('#content');
|
|
|
|
//set the menu hover and hoverout states
|
|
$menu.find('li.menu').each(function () {
|
|
var $this = $(this),
|
|
$submenu = $this.children('ul');
|
|
|
|
//bind events for the top-level menu item
|
|
$this.bind({
|
|
mouseenter: function () {
|
|
clearTimeout($this.data('timer'));
|
|
$this.addClass('current');
|
|
},
|
|
mouseleave: function () {
|
|
$this.data('timer', setTimeout(function () {
|
|
$submenu.fadeOut(150);
|
|
$this.removeClass('current');
|
|
}, 150));
|
|
}
|
|
});
|
|
|
|
//make the submenu slide down on hover
|
|
$this.hover(function () {
|
|
//if this is a sub-submenu, slide it right instead of down
|
|
if ($this.parent().closest('li.menu').length) {
|
|
$this.addClass('current');
|
|
$submenu.stop(true, true).show('slide', {direction: 'left'}, 200);
|
|
}
|
|
else
|
|
$submenu.stop(true, true).slideDown(200);
|
|
});
|
|
});
|
|
|
|
toggleMenu = function (toggle) {
|
|
$menuButton.toggleClass('current', toggle);
|
|
|
|
if (toggle)
|
|
$mobileMenu.stop(true, true).show('slide', {direction: 'left'}, 100);
|
|
else
|
|
$mobileMenu.stop(true, true).hide('slide', {direction: 'left'}, 100);
|
|
}
|
|
|
|
toggleFilter = function (toggle) {
|
|
$filterButton.toggleClass('current', toggle);
|
|
$filters.toggleClass('shown', toggle);
|
|
$content.toggleClass('hidden', toggle);
|
|
|
|
admin.resizePage();
|
|
}
|
|
|
|
//clicking the menu button hides/shows the mobile menu
|
|
$menuButton.click(function (e) {
|
|
e.preventDefault();
|
|
|
|
toggleMenu(!$menuButton.hasClass('current'));
|
|
});
|
|
|
|
//clicking the filter button hides/shows the filter
|
|
$filterButton.click(function (e) {
|
|
e.preventDefault();
|
|
|
|
toggleFilter(!$filterButton.hasClass('current'));
|
|
});
|
|
|
|
//hide the menu on document click outside
|
|
$(document).click(function (e) {
|
|
var inMenuButton = $menuButton.is(e.target) || $menuButton.has(e.target).length !== 0,
|
|
inMenu = $mobileMenu.is(e.target) || $mobileMenu.has(e.target).length !== 0,
|
|
inFilterButton = $filterButton.is(e.target) || $filterButton.has(e.target).length !== 0,
|
|
inFilters = $filters.is(e.target) || $filters.has(e.target).length !== 0;
|
|
|
|
if ($menuButton.hasClass('current') && !inMenu && !inMenuButton)
|
|
toggleMenu(false);
|
|
|
|
if ($filterButton.hasClass('current') && !inFilters && !inFilterButton)
|
|
toggleFilter(false);
|
|
});
|
|
|
|
//clicking menu items in the mobile menu hides/shows that submenu
|
|
$mobileMenu.on('click', 'li.menu > span', function () {
|
|
$(this).siblings('ul').toggle();
|
|
});
|
|
|
|
//set up the customscroll plugin for the mobile menu
|
|
$mobileMenu.customscroll();
|
|
|
|
|
|
//disable body scroll when scroll a scrollable content
|
|
$('.scrollable').on('DOMMouseScroll mousewheel', function (ev) {
|
|
var $this = $(this),
|
|
scrollTop = this.scrollTop,
|
|
scrollHeight = this.scrollHeight,
|
|
height = $this.height(),
|
|
delta = (ev.type == 'DOMMouseScroll' ?
|
|
ev.originalEvent.detail * -40 :
|
|
ev.originalEvent.wheelDelta),
|
|
up = delta > 0;
|
|
|
|
var prevent = function () {
|
|
ev.stopPropagation();
|
|
ev.preventDefault();
|
|
ev.returnValue = false;
|
|
return false;
|
|
};
|
|
|
|
if (!up && -delta > scrollHeight - height - scrollTop) {
|
|
// Scrolling down, but this will take us past the bottom.
|
|
$this.scrollTop(scrollHeight);
|
|
return prevent();
|
|
} else if (up && delta > scrollTop) {
|
|
// Scrolling up, but this will take us past the top.
|
|
$this.scrollTop(0);
|
|
return prevent();
|
|
}
|
|
});
|
|
|
|
// filter btn
|
|
$('#filter-btn-success').on('click', function () {
|
|
var visible = $('#sidebar').is(':visible');
|
|
|
|
if (visible) {
|
|
$('.item_edit_container').fadeIn();
|
|
$('#sidebar').fadeOut();
|
|
} else {
|
|
$('.item_edit_container').fadeOut();
|
|
$('#sidebar').fadeIn();
|
|
}
|
|
});
|
|
});
|
|
})(jQuery);
|
|
|
|
//fixes the issue with media queries not firing when the user resizes the browser in another tab
|
|
(function () {
|
|
var hidden = "hidden";
|
|
|
|
// Standards:
|
|
if (hidden in document)
|
|
document.addEventListener("visibilitychange", onchange);
|
|
else if ((hidden = "mozHidden") in document)
|
|
document.addEventListener("mozvisibilitychange", onchange);
|
|
else if ((hidden = "webkitHidden") in document)
|
|
document.addEventListener("webkitvisibilitychange", onchange);
|
|
else if ((hidden = "msHidden") in document)
|
|
document.addEventListener("msvisibilitychange", onchange);
|
|
// IE 9 and lower:
|
|
else if ('onfocusin' in document)
|
|
document.onfocusin = document.onfocusout = onchange;
|
|
// All others:
|
|
else
|
|
window.onpageshow = window.onpagehide
|
|
= window.onfocus = window.onblur = onchange;
|
|
|
|
function onchange(evt) {
|
|
var v = 'sg-tab-bust-visible', h = 'sg-tab-bust-hidden',
|
|
evtMap = {
|
|
focus: v, focusin: v, pageshow: v, blur: h, focusout: h, pagehide: h
|
|
};
|
|
|
|
evt = evt || window.event;
|
|
if (evt.type in evtMap)
|
|
document.body.className = evtMap[evt.type];
|
|
else
|
|
document.body.className = this[hidden] ? "sg-tab-bust-hidden" : "sg-tab-bust-visible";
|
|
|
|
//clear out the body's class
|
|
document.body.className = '';
|
|
}
|
|
})();
|
|
|
|
//# sourceMappingURL=app.js.map
|