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