714 lines
26 KiB
JavaScript
Vendored
714 lines
26 KiB
JavaScript
Vendored
(function ($) {
|
|
|
|
/**
|
|
* For the item form transition
|
|
*/
|
|
ko.bindingHandlers.itemTransition = {
|
|
init: function (element, valueAccessor, allBindingsAccessor, viewModel, context) {
|
|
var $element = $(element),
|
|
viewModel = context.$root,
|
|
$child = $element.find('.item_edit'),
|
|
$tableContainer = $('div.table_container'),
|
|
expandWidth = viewModel.expandWidth();
|
|
|
|
//the lastItem gets reset to null when the form is closed. This way we can draw the table properly initially
|
|
//so that it doesn't keep reopening.
|
|
if (viewModel.lastItem === null) {
|
|
$tableContainer.css('margin-right', 290);
|
|
$element.hide();
|
|
$child.css('marginLeft', expandWidth + 2);
|
|
}
|
|
else {
|
|
$tableContainer.css('margin-right', expandWidth + 5);
|
|
$child.css('marginLeft', 2);
|
|
}
|
|
},
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel, context) {
|
|
var $element = $(element),
|
|
viewModel = context.$root,
|
|
$child = $element.find('.item_edit'),
|
|
$tableContainer = $('div.table_container'),
|
|
expandWidth = viewModel.expandWidth();
|
|
|
|
//if the value is false, we want to hide the form, otherwise show it
|
|
if (!valueAccessor()) {
|
|
$child.stop().animate({marginLeft: expandWidth + 2}, 150, function () {
|
|
$element.hide();
|
|
});
|
|
|
|
$tableContainer.stop().animate({marginRight: 290}, 150, function () {
|
|
window.admin.resizePage();
|
|
});
|
|
}
|
|
else {
|
|
if (viewModel.lastItem === null) {
|
|
$element.show();
|
|
$child.stop().animate({marginLeft: 2}, 150);
|
|
$tableContainer.stop().animate({marginRight: expandWidth + 5}, 150, function () {
|
|
window.admin.resizePage();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
var select2Defaults = {
|
|
placeholder: adminData.languages['select_options'],
|
|
formatNoMatches: function (term) {
|
|
return adminData.languages['no_results'];
|
|
},
|
|
width: 'resolve',
|
|
allowClear: true
|
|
};
|
|
|
|
//for select2
|
|
ko.bindingHandlers.select2 = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor(),
|
|
defaults = $.extend({}, select2Defaults),
|
|
data;
|
|
|
|
if (options && typeof options === 'object') {
|
|
$.extend(defaults, options);
|
|
}
|
|
|
|
//pull the latest from the list
|
|
if (defaults.data) {
|
|
if ($.isFunction(defaults.data.results)) {
|
|
defaults.data.results = options.data.results();
|
|
}
|
|
|
|
$(element).data('list_data', defaults.data.results);
|
|
|
|
defaults.data = function () {
|
|
return {results: $(element).data('list_data')};
|
|
}
|
|
}
|
|
|
|
//init select2 if it isn't already set up
|
|
if ($(element).data("select2") === undefined || $(element).data("select2") === null) {
|
|
//set the original list data in case we need it for sorting
|
|
$(element).data('original_list_data', [].concat($(element).data('list_data')));
|
|
|
|
$(element).select2(defaults);
|
|
|
|
//if the sort option is set, set up jquery ui sortable
|
|
if (options.sort) {
|
|
$(element).select2('container').find('ul.select2-choices').sortable({
|
|
containment: 'parent',
|
|
start: function () {
|
|
$(element).select2("onSortStart")
|
|
},
|
|
update: function () {
|
|
$(element).select2("onSortEnd")
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
//it's necessary to reorder the options array if the sort is set
|
|
if (options.sort) {
|
|
var listData = $(element).data('list_data'),
|
|
val = $(element).val();
|
|
|
|
//initially we want to reset the list data so we can work with a fresh, alphabetized sort
|
|
$(element).data('list_data', [].concat($(element).data('original_list_data')));
|
|
|
|
//if there is a value for this field, split it and find the relevant items in the array
|
|
if (val) {
|
|
var vals = val.split(','),
|
|
topItems = [],
|
|
allItems = $(element).data('list_data');
|
|
|
|
//iterate over the values
|
|
$.each(vals, function (ind, el) {
|
|
//iterate over all the items to find our value
|
|
$.each(allItems, function (i, e) {
|
|
if (e.id == el) {
|
|
topItems.push(e);
|
|
allItems.splice(i, 1);
|
|
return false;
|
|
}
|
|
});
|
|
});
|
|
|
|
$(element).data('list_data', topItems.concat(allItems));
|
|
}
|
|
}
|
|
|
|
//make sure we're monitoring the change event for page resizing
|
|
$(element).on('change', function () {
|
|
window.admin.resizePage();
|
|
});
|
|
|
|
setTimeout(function () {
|
|
$(element).trigger('change');
|
|
}, 50);
|
|
}
|
|
};
|
|
|
|
var select2RemoteHandler = function (element, valueAccessor, allBindingsAccessor, viewModel, context) {
|
|
var options = valueAccessor(),
|
|
defaults = $.extend({
|
|
minimumInputLength: 1,
|
|
allowClear: true,
|
|
ajax: {
|
|
url: base_url + adminData.model_name + '/update_options',
|
|
dataType: 'json',
|
|
quietMillis: 100,
|
|
type: 'POST',
|
|
data: function (term, page) {
|
|
var data = {
|
|
term: term,
|
|
page: page,
|
|
field: options.field,
|
|
type: options.type,
|
|
constraints: {}
|
|
};
|
|
|
|
if (data.type === 'edit') {
|
|
data.selectedItems = admin.viewModel[data.field]();
|
|
}
|
|
else if (data.type === 'filter') {
|
|
data.selectedItems = admin.filtersViewModel.filters[parseInt(options.filterIndex)].value();
|
|
}
|
|
|
|
//figure out if there are any constraints that we need to send over
|
|
if (options.constraints) {
|
|
$.each(options.constraints, function (ind, el) {
|
|
data.constraints[ind] = admin.viewModel[ind]();
|
|
});
|
|
}
|
|
|
|
return {fields: [data]};
|
|
},
|
|
results: function (returndata, page) {
|
|
var data = {},
|
|
val = $(element).val();
|
|
|
|
//we want to update the autocomplete index so we can show all possibly-selected items
|
|
if (val) {
|
|
$(val.split(',')).each(function (ind, el) {
|
|
data[this] = {
|
|
id: this,
|
|
text: admin.viewModel[options.field + '_autocomplete'][this].text
|
|
};
|
|
});
|
|
}
|
|
|
|
//iterate over the results and put them in the autocomplete array
|
|
$.each(returndata[options.field], function (ind, el) {
|
|
data[el.id] = el;
|
|
});
|
|
|
|
admin.viewModel[options.field + '_autocomplete'] = data;
|
|
|
|
return {
|
|
results: returndata[options.field]
|
|
}
|
|
}
|
|
},
|
|
initSelection: function (element, callback) {
|
|
var data = [],
|
|
val = $(element).val();
|
|
|
|
// If the select2 field has a default value,
|
|
// initSelection will be called before the admin object
|
|
// is correctly initialized.
|
|
if (!val || typeof admin === 'undefined')
|
|
return callback(null);
|
|
|
|
//if this is a multi-select, set up the data as an array
|
|
if (options.multiple) {
|
|
$(element.val().split(',')).each(function (ind, el) {
|
|
if (this in admin.viewModel[options.field + '_autocomplete'])
|
|
data.push({
|
|
id: this,
|
|
text: admin.viewModel[options.field + '_autocomplete'][this].text
|
|
});
|
|
});
|
|
}
|
|
//otherwise make the data a simple object
|
|
else {
|
|
if (val in admin.viewModel[options.field + '_autocomplete'])
|
|
data = {id: val, text: admin.viewModel[options.field + '_autocomplete'][val].text};
|
|
}
|
|
|
|
callback(data);
|
|
}
|
|
}, select2Defaults);
|
|
|
|
if (options && typeof options === 'object') {
|
|
$.extend(defaults, options);
|
|
}
|
|
|
|
//init select2 if it isn't already set up
|
|
if ($(element).data("select2") === undefined || $(element).data("select2") === null) {
|
|
$(element).select2(defaults);
|
|
|
|
//if the sort option is set, set up jquery ui sortable
|
|
if (options.sort) {
|
|
$(element).select2('container').find('ul.select2-choices').sortable({
|
|
containment: 'parent',
|
|
start: function () {
|
|
$(element).select2("onSortStart")
|
|
},
|
|
update: function () {
|
|
$(element).select2("onSortEnd")
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
setTimeout(function () {
|
|
$(element).trigger('change');
|
|
}, 50);
|
|
}
|
|
|
|
//for ajax/remote select2
|
|
ko.bindingHandlers.select2Remote = {
|
|
update: select2RemoteHandler
|
|
};
|
|
|
|
/**
|
|
* The number binding ensures that a value is decimal-like
|
|
*/
|
|
ko.bindingHandlers.number = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor(),
|
|
value = allBindingsAccessor().value(),
|
|
floatVal,
|
|
$element = $(element);
|
|
|
|
//if this is a null or false value, run a parseFloat on it so we can check for isNaN later
|
|
if (value === null || value === false) {
|
|
floatVal = parseFloat(value);
|
|
}
|
|
//else we will try to parse the number using the user-supplied thousands and decimal separators
|
|
else {
|
|
floatVal = parseFloat(value.toString().trim().split(options.thousandsSeparator).join('').split(options.decimalSeparator).join('.'));
|
|
}
|
|
|
|
//if the value is not a number, set the value equal to ''
|
|
if (isNaN(floatVal)) {
|
|
allBindingsAccessor().value(null);
|
|
|
|
//if this is an uneditable field, set the text
|
|
if ($element.hasClass('uneditable'))
|
|
$element.text('');
|
|
//otherwise we know it's an input
|
|
else
|
|
$element.val('');
|
|
}
|
|
//else set up the value up using the accounting library with the user-supplied separators
|
|
else {
|
|
//if this is an uneditable field, set the text
|
|
if ($element.hasClass('uneditable'))
|
|
$element.text(accounting.formatMoney(floatVal, "", options.decimals, options.thousandsSeparator, options.decimalSeparator));
|
|
//otherwise we know it's an input
|
|
else
|
|
$element.val(accounting.formatMoney(floatVal, "", options.decimals, options.thousandsSeparator, options.decimalSeparator));
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The datepicker binding makes sure the jQuery UI datepicker is set for this item
|
|
*/
|
|
ko.bindingHandlers.datepicker = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor();
|
|
|
|
$(element).datepicker({
|
|
dateFormat: options.dateFormat
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The formatDate binding transforms a date string into a formatted date
|
|
*/
|
|
ko.bindingHandlers.formatDate = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor(),
|
|
dateVal = options.value.length === 10 ? options.val + ' 00:00' : options.val;
|
|
|
|
$(element).text($.datepicker.formatDate(options.dateFormat, new Date(options.value)));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The timepicker binding makes sure the jQuery UI timepicker is set for this item
|
|
*/
|
|
ko.bindingHandlers.timepicker = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor(),
|
|
val = allBindingsAccessor().value(),
|
|
date = new Date('01/01/2013 ' + val),
|
|
timeObject = {
|
|
hour: date.getHours(),
|
|
minute: date.getMinutes()
|
|
};
|
|
|
|
if (val)
|
|
$(element).val($.datepicker.formatTime(options.timeFormat, timeObject));
|
|
|
|
$(element).timepicker({
|
|
timeFormat: options.timeFormat
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The formatTime binding transforms a time string into a formatted time
|
|
*/
|
|
ko.bindingHandlers.formatTime = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor(),
|
|
date = new Date('01/01/2012 ' + options.value),
|
|
timeObject = {
|
|
hour: date.getHours(),
|
|
minute: date.getMinutes()
|
|
};
|
|
|
|
$(element).text($.datepicker.formatTime(options.timeFormat, timeObject));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The datetimepicker binding makes sure the jQuery UI datetimepicker is set for this item
|
|
*/
|
|
ko.bindingHandlers.datetimepicker = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor(),
|
|
val = allBindingsAccessor().value(),
|
|
date = new Date(val),
|
|
timeObject = {
|
|
hour: date.getHours(),
|
|
minute: date.getMinutes()
|
|
};
|
|
|
|
if (val && !isNaN(date.getHours())) {
|
|
|
|
var formattedDate = $.datepicker.formatDate(options.dateFormat, date),
|
|
formattedTime = $.datepicker.formatTime(options.timeFormat, timeObject);
|
|
|
|
$(element).val(formattedDate + ' ' + formattedTime);
|
|
}
|
|
|
|
$(element).datetimepicker({
|
|
dateFormat: options.dateFormat,
|
|
timeFormat: options.timeFormat
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The formatTime binding transforms a datetime string into a formatted datetime
|
|
*/
|
|
ko.bindingHandlers.formatDateTime = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor(),
|
|
date = new Date(options.value),
|
|
timeObject = {
|
|
hour: date.getHours(),
|
|
minute: date.getMinutes()
|
|
};
|
|
|
|
if (!isNaN(date.getHours())) {
|
|
var formattedDate = $.datepicker.formatDate(options.dateFormat, date),
|
|
formattedTime = $.datepicker.formatTime(options.timeFormat, timeObject);
|
|
|
|
$(element).text(formattedDate + ' ' + formattedTime);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The characterLimit binding makes sure a text field only has so many characters
|
|
*/
|
|
ko.bindingHandlers.characterLimit = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var limit = valueAccessor(),
|
|
val = allBindingsAccessor().value();
|
|
|
|
val = val === null ? '' : val + '';
|
|
|
|
if (!limit || val === null || val.length < limit)
|
|
return;
|
|
|
|
val = val.substr(0, limit);
|
|
|
|
$(element).val(val);
|
|
allBindingsAccessor().value(val);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The charactersLeft binding fills the element with (#chars allowed - #chars typed)
|
|
*/
|
|
ko.bindingHandlers.charactersLeft = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor(),
|
|
limit = options.limit,
|
|
val = options.value();
|
|
|
|
val = val === null ? '' : val + '';
|
|
|
|
//if the limit is zero, there is no limit
|
|
if (!limit)
|
|
return;
|
|
|
|
//if the value is null, set it to an empty string
|
|
if (val === null)
|
|
val = '';
|
|
|
|
left = limit - val.length;
|
|
|
|
// text = ' character' + (left !== 1 ? 's' : '') + ' left';
|
|
text = (left !== 1 ? adminData.languages['characters_left'] : adminData.languages['character_left']);
|
|
|
|
$(element).text(left + text);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* This ensures that a bool field is always a boolean value
|
|
*/
|
|
ko.bindingHandlers.bool = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel, context) {
|
|
var viewModel = context.$root,
|
|
modelVal = viewModel[valueAccessor()]();
|
|
|
|
if (modelVal === '0')
|
|
viewModel[valueAccessor()](false);
|
|
else if (modelVal === '1')
|
|
viewModel[valueAccessor()](true);
|
|
}
|
|
};
|
|
|
|
var editors = {};
|
|
|
|
/**
|
|
* The wysiwyg binding makes the field a ckeditor wysiwyg
|
|
*/
|
|
ko.bindingHandlers.wysiwyg = {
|
|
init: function (element, valueAccessor, allBindingsAccessor, context) {
|
|
var options = valueAccessor(),
|
|
value = ko.utils.unwrapObservable(options.value),
|
|
$element = $(element),
|
|
editor;
|
|
|
|
value = value ? value : '';
|
|
|
|
$element.html(value);
|
|
|
|
if (options.id in editors)
|
|
editor = editors[options.id];
|
|
else {
|
|
$element.ckeditor({
|
|
language: language,
|
|
readOnly: !adminData.edit_fields[context.field_name].editable
|
|
});
|
|
|
|
editor = $element.ckeditorGet();
|
|
editors[options.id] = editor;
|
|
}
|
|
|
|
//when the editor is loaded, we want to resize our page
|
|
editor.on('loaded', function () {
|
|
setTimeout(function () {
|
|
window.admin.resizePage();
|
|
}, 50);
|
|
|
|
editor.on('resize', function () {
|
|
window.admin.resizePage();
|
|
});
|
|
});
|
|
|
|
//wire up the blur event to ensure our observable is properly updated
|
|
editor.focusManager.blur = function () {
|
|
var observable = valueAccessor().value,
|
|
$el = $('#' + options.id);
|
|
|
|
//set the blur attribute to true so we know now to set the editor data in the update method
|
|
$el.data('blur', true);
|
|
|
|
observable($el.val());
|
|
}
|
|
|
|
//handle destroying an editor (based on what jQuery plugin does)
|
|
ko.utils.domNodeDisposal.addDisposeCallback(element, function (test) {
|
|
var editor = editors[options.id];
|
|
|
|
if (editor) {
|
|
editor.destroy();
|
|
delete editors[options.id];
|
|
}
|
|
});
|
|
},
|
|
update: function (element, valueAccessor, allBindingsAccessor, context) {
|
|
//handle programmatic updates to the observable
|
|
var options = valueAccessor(),
|
|
value = ko.utils.unwrapObservable(options.value),
|
|
$element = $(element),
|
|
editor = editors[options.id];
|
|
|
|
value = value ? value : '';
|
|
|
|
//if there isn't a value, set the value immediately
|
|
if (!value) {
|
|
$element.html(value);
|
|
editor.setData(value);
|
|
}
|
|
//otherwise pause for a moment and then set it
|
|
else {
|
|
setTimeout(function () {
|
|
$element.html(value);
|
|
|
|
if ($element.data('blur'))
|
|
$element.removeData('blur');
|
|
else
|
|
editor.setData(value);
|
|
|
|
}, 50);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The markdown binding is attached to the field next a markdown textarea
|
|
*/
|
|
ko.bindingHandlers.markdown = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, context) {
|
|
//handle programmatic updates to the observable
|
|
var value = ko.utils.unwrapObservable(valueAccessor());
|
|
|
|
if (!value) {
|
|
$(element).html(value);
|
|
}
|
|
else {
|
|
$(element).html(markdown.toHTML(value.toString()));
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The enumText binding converts a value and an options array to a "Label (value)" readable format
|
|
*/
|
|
ko.bindingHandlers.enumText = {
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
|
|
var options = valueAccessor(),
|
|
value = options.value,
|
|
enumOptions = options.enumOptions;
|
|
|
|
for (var i = 0; i < enumOptions.length; i++) {
|
|
if (enumOptions[i].id == value) {
|
|
$(element).html(enumOptions[i].text + " (" + value + ")");
|
|
return;
|
|
}
|
|
}
|
|
|
|
$(element).html(value);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* File uploader using plupload
|
|
*/
|
|
ko.bindingHandlers.fileupload = {
|
|
init: function (element, valueAccessor, allBindingsAccessor, viewModel, context) {
|
|
var options = valueAccessor(),
|
|
cacheName = options.field + '_uploader',
|
|
viewModel = context.$root,
|
|
filters = options.image ? [{title: 'Image files', extensions: 'jpg,jpeg,gif,png'}] : [];
|
|
|
|
viewModel[cacheName] = new plupload.Uploader({
|
|
runtimes: 'html5,flash,silverlight,gears,browserplus',
|
|
browse_button: cacheName,
|
|
container: 'edit_field_' + options.field,
|
|
drop_element: cacheName,
|
|
multi_selection: false,
|
|
max_file_size: options.size_limit + 'mb',
|
|
url: options.upload_url,
|
|
flash_swf_url: asset_url + 'js/plupload/js/plupload.flash.swf',
|
|
silverlight_xap_url: asset_url + 'js/plupload/js/plupload.silverlight.xap',
|
|
filters: filters,
|
|
multipart_params: {"_token": window.csrf}
|
|
});
|
|
|
|
viewModel[cacheName].init();
|
|
|
|
viewModel[cacheName].bind('FilesAdded', function (up, files) {
|
|
|
|
viewModel.freezeActions(true);
|
|
|
|
$(files).each(function (i, file) {
|
|
//parent.uploader.removeFile(file);
|
|
|
|
});
|
|
|
|
options.upload_percentage(0);
|
|
options.uploading(true);
|
|
|
|
viewModel[cacheName].start();
|
|
});
|
|
|
|
viewModel[cacheName].bind('UploadProgress', function (up, file) {
|
|
options.upload_percentage(file.percent);
|
|
});
|
|
|
|
viewModel[cacheName].bind('Error', function (up, err) {
|
|
alert(err.message);
|
|
});
|
|
|
|
viewModel[cacheName].bind('FileUploaded', function (up, file, response) {
|
|
var data = JSON.parse(response.response);
|
|
|
|
options.uploading(false);
|
|
|
|
if (data.errors.length === 0) {
|
|
//success
|
|
//iterate over the files until we find it and then set the proper fields
|
|
viewModel[options.field](data.filename);
|
|
} else {
|
|
//error
|
|
alert(data.errors);
|
|
}
|
|
|
|
setTimeout(function () {
|
|
viewModel[cacheName].splice();
|
|
viewModel[cacheName].refresh();
|
|
$('div.plupload').css('z-index', 71);
|
|
viewModel.freezeActions(false);
|
|
admin.resizePage();
|
|
}, 200);
|
|
});
|
|
|
|
$('#' + cacheName).bind('dragenter', function (e) {
|
|
$(this).addClass('drag');
|
|
});
|
|
|
|
$('#' + cacheName).bind('dragleave drop', function (e) {
|
|
$(this).removeClass('drag');
|
|
});
|
|
|
|
//destroy the existing editor if the DOM node is removed
|
|
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
|
|
viewModel[cacheName].destroy();
|
|
});
|
|
},
|
|
update: function (element, valueAccessor, allBindingsAccessor, viewModel, context) {
|
|
var options = valueAccessor(),
|
|
cacheName = options.field + '_uploader',
|
|
viewModel = context.$root;
|
|
|
|
//hack to get the z-index properly set up
|
|
setTimeout(function () {
|
|
viewModel[cacheName].refresh();
|
|
$('div.plupload').css('z-index', 71);
|
|
}, 200);
|
|
}
|
|
};
|
|
|
|
})(jQuery); |