(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);