(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('
') }, /** * 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