laravel-learn-bbs/public/packages/summerblue/administrator/js/app.js

2875 lines
100 KiB
JavaScript
Raw Normal View History

2018-02-12 15:56:05 +00:00
(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