Created
October 5, 2014 22:24
-
-
Save optilude/90d6a574a3ba4186f816 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
define([ | |
'jquery', | |
'underscore', | |
'app/app', | |
'moment', | |
'numeral', | |
'app/utils/dateutils', | |
'jquery.handsontable', | |
'bootstrap.datepicker' | |
], function($, _, app, moment, numeral, DateUtils, Handsontable) { | |
"use strict"; | |
// XXX: Why on earth is this necessary? | |
if(Handsontable === undefined) { | |
Handsontable = window.Handsontable; | |
} | |
// General handler tracking table changes | |
Handsontable.PluginHooks.add('afterChange', function(changes, source) { | |
if(this.getSettings().allowUnload !== true && _.contains(["alter", "empty", "edit", "autofill", "paste"], source)) { | |
app.dirty = true; | |
} | |
}); | |
// Helper to avoid validating blank spare rows | |
function entryValidator(validator) { | |
return function(value, callback) { | |
var table = this.instance, | |
maxRow = table.countRows() - table.getSettings().minSpareRows - 1; | |
if(this.row > maxRow) { | |
callback(true); | |
return; | |
} | |
validator.call(this, value, callback); | |
}; | |
} | |
// Date editor using bootstrap.datepicker | |
var DateEditor = (function (Handsontable) { | |
var BootstrapDateEditor = Handsontable.editors.TextEditor.prototype.extend(); | |
BootstrapDateEditor.prototype.init = function () { | |
var self = this; | |
Handsontable.editors.TextEditor.prototype.init.apply(self, arguments); | |
self.isCellEdited = false; | |
self.instance.addHook('afterDestroy', function () { | |
self.destroyElements(); | |
}); | |
}; | |
BootstrapDateEditor.prototype.createElements = function () { | |
var self = this; | |
Handsontable.editors.TextEditor.prototype.createElements.apply(self, arguments); | |
self.datePickerHolder = document.createElement('DIV'); | |
this.datePickerHolderStyle = this.datePickerHolder.style; | |
this.instance.view.wt.wtDom.addClass(this.datePickerHolder, 'htDatepickerHolder'); | |
document.body.appendChild(self.datePickerHolder); | |
self.$datePickerHolder = $(self.datePickerHolder); | |
self.$datePickerHolder.datepicker({ | |
format: app.datePickerHolderDateFormat, | |
weekStart: 1 | |
}).on('changeDate', function(event) { | |
self.setValue(moment.utc(event.date).format(app.momentDateFormat)); | |
self.hideDatepicker(); | |
self.finishEditing(false); | |
}); | |
// prevent recognizing clicking on date picker as clicking outside of table | |
self.$datePickerHolder.on('mousedown', function (event) { | |
event.stopPropagation(); | |
}); | |
self.hideDatepicker(); | |
}; | |
BootstrapDateEditor.prototype.destroyElements = function () { | |
this.$datePickerHolder.datepicker('remove'); | |
this.$datePickerHolder.remove(); | |
}; | |
BootstrapDateEditor.prototype.beginEditing = function (initialValue) { | |
if(!initialValue && this.originalValue) { | |
initialValue = moment.utc(this.originalValue).format(app.momentDateFormat); | |
} | |
Handsontable.editors.TextEditor.prototype.beginEditing.call(this, initialValue); | |
this.showDatepicker(); | |
}; | |
BootstrapDateEditor.prototype.finishEditing = function (isCancelled, ctrlDown) { | |
this.hideDatepicker(); | |
Handsontable.editors.TextEditor.prototype.finishEditing.apply(this, arguments); | |
}; | |
BootstrapDateEditor.prototype.saveValue = function(val, ctrlDown){ | |
for(var i = 0; i < val.length; ++i) { | |
for(var j = 0; j < val[i].length; ++j) { | |
if(_.isString(val[i][j])) { | |
var d = moment.utc(val[i][j], app.momentDateFormat); | |
if(d.isValid()) { | |
val[i][j] = d.toDate(); | |
} | |
} | |
} | |
} | |
Handsontable.editors.TextEditor.prototype.saveValue.call(this, val, ctrlDown); | |
}; | |
BootstrapDateEditor.prototype.showDatepicker = function () { | |
var $td = $(this.TD); | |
var offset = $td.offset(); | |
this.datePickerHolderStyle.top = (offset.top + $td.height()) + 'px'; | |
this.datePickerHolderStyle.left = offset.left + 'px'; | |
var dateOptions = { | |
defaultDate: this.originalValue || void 0 | |
}; | |
$.extend(dateOptions, this.cellProperties); | |
this.$datePickerHolder.datepicker("option", dateOptions); | |
if (this.originalValue) { | |
this.$datePickerHolder.datepicker('setDate', moment.utc(this.originalValue).toDate()); | |
} | |
this.$datePickerHolder.show(); | |
this.$datePickerHolder.datepicker('show'); | |
}; | |
BootstrapDateEditor.prototype.hideDatepicker = function () { | |
this.$datePickerHolder.datepicker('hide'); | |
this.$datePickerHolder.hide(); | |
}; | |
return BootstrapDateEditor; | |
})(Handsontable); | |
/** | |
* Autocomplete editor that can store a value separately to its displayed | |
* value. Uses a "handsontable-in-handsontable" editor. | |
* | |
* Requires the following configuration: | |
* | |
* - `source`, an array of arrays or a function that takes `query` and | |
* `callback` parameters like the one used by `AutocompleteEditor`. | |
* The data passed should be an array of objects or array of arrays, | |
* akin to the `data` of a Handsontable instance. | |
* - `handsontable`, an object with settings for the inner table. Typical | |
* settings include `colWidths`, `colHeaders`, `dataSchema` and | |
* `columns`. | |
* - `extractValue`, a function that will be passed a selected row from | |
* the `source` data and which should return the *value* to store in | |
* the underlying handsontable. | |
* - `extractTitle`, a function that will be passed a selected row from | |
* the `source` data and which should return the *title* to show when | |
* opening the editor | |
* | |
* For example: | |
* | |
* { | |
* data: "user", | |
* editor: KeyValueAutocompleteEditor | |
* handsontable: { | |
* colWidths: [140, 196], | |
* colHeaders: [Name", "Email"], | |
* dataSchema: {name: null, email: null}, | |
* columns: [{ | |
* data: 'name' | |
* }, { | |
* data: 'email' | |
* }] | |
* }, | |
* extractValue: function(row) { return row.userId; }, | |
* extractTitle: function(row) { return row.name; }, | |
* source: function(query, callback) { | |
* | |
* if(query.length < 2) { | |
* return callback([]); | |
* } | |
* | |
* // This would more likely use an AJAX call or some other | |
* // mechanism to build an array of arrays of possible values | |
* callback([{userId: 1, name: 'User 1', email: '[email protected]'}, | |
* {userId: 2, name: 'User 2', email: '[email protected]'}]); | |
* | |
* }, | |
* } | |
* | |
* When a row is chosen in the editor the value returned by `extractValue()` | |
* is saved in the underlying handsontable, but a "hint" is also stored in | |
* the relevant row of the underlying handsontable data array. The hint has | |
* a key of `_<prop>` where <prop> is the name of the column, so in the | |
* example above, that would be `_user`. The hint contains the full row of | |
* the inner handsontable. In the example above, that would be a single | |
* object with keys `userId`, `name` and `email`. | |
* | |
* You can load the hint object into the data array before the editor is | |
* invoked for the first time to let the editor start with the correct | |
* value. You can also use this in a cell renderer to render the title | |
* rather than the data: | |
* | |
* renderer: function(instance, td, row, col, prop, value, cellProperties) { | |
* if(value !== undefined && value !== null) { | |
* var hint = instance.getDataAtRow(row)['_' + prop]; | |
* | |
* if(hint !== undefined) { | |
* value = cellProperties.extractTitle(hint); | |
* } | |
* } | |
* | |
* Handsontable.AutocompleteRenderer(instance, td, row, col, prop, value, cellProperties); | |
* } | |
* | |
*/ | |
var KeyValueAutocompleteEditor = (function() { | |
var KeyValueAutocompleteEditor = Handsontable.editors.HandsontableEditor.prototype.extend(); | |
KeyValueAutocompleteEditor.prototype.init = function () { | |
Handsontable.editors.HandsontableEditor.prototype.init.apply(this, arguments); | |
this.query = null; | |
this.choices = []; | |
}; | |
KeyValueAutocompleteEditor.prototype.prepare = function () { | |
Handsontable.editors.HandsontableEditor.prototype.prepare.apply(this, arguments); | |
this.hintProp = '_' + this.prop; | |
// Make the prototype behave as we want | |
this.cellProperties.filter = false; | |
this.cellProperties.strict = true; | |
this.keyTimeout = this.cellProperties.keyTimeout || 0; | |
}; | |
KeyValueAutocompleteEditor.prototype.beginEditing = function (initialValue) { | |
// Set initial editor value based on the title that was last used | |
var hint = this.instance.getDataAtRow(this.row)[this.hintProp]; | |
if(hint !== undefined && this.cellProperties.extractTitle !== undefined) { | |
initialValue = this.cellProperties.extractTitle(hint); | |
} | |
Handsontable.editors.HandsontableEditor.prototype.beginEditing.apply(this, [initialValue]); | |
}; | |
KeyValueAutocompleteEditor.prototype.createElements = function(){ | |
Handsontable.editors.HandsontableEditor.prototype.createElements.apply(this, arguments); | |
this.$htContainer.addClass('keyValueAutocompleteEditor'); | |
}; | |
KeyValueAutocompleteEditor.prototype.bindEvents = function(){ | |
var that = this, | |
timer = null; | |
this.$textarea.on('keydown.autocompleteEditor', function(event){ | |
if(event.altKey || event.ctrlKey || event.metaKey || | |
(Handsontable.helper.isMetaKey(event.keyCode) && [Handsontable.helper.keyCode.BACKSPACE, Handsontable.helper.keyCode.DELETE].indexOf(event.keyCode) === -1)) { | |
return; | |
} | |
// Don't run query until user stops typing | |
if(timer !== null) { | |
clearTimeout(timer); | |
} | |
timer = setTimeout(function () { | |
that.queryChoices(that.$textarea.val()); | |
}, that.keyTimeout); | |
}); | |
this.$htContainer.on('mouseleave', function () { | |
that.highlightBestMatchingChoice(); | |
}); | |
this.$htContainer.on('mouseenter', function () { | |
that.$htContainer.handsontable('deselectCell'); | |
}); | |
Handsontable.editors.HandsontableEditor.prototype.bindEvents.apply(this, arguments); | |
}; | |
var onBeforeKeyDownInner; | |
KeyValueAutocompleteEditor.prototype.open = function () { | |
Handsontable.editors.HandsontableEditor.prototype.open.apply(this, arguments); | |
this.$textarea[0].style.visibility = 'visible'; | |
this.focus(); | |
var choicesListHot = this.$htContainer.handsontable('getInstance'); | |
var that = this; | |
choicesListHot.updateSettings({ | |
afterRenderer: function (TD, row, col, prop, value) { | |
if(!_.isString(value)){ | |
return; | |
} | |
var caseSensitive = this.getCellMeta(row, col).filteringCaseSensitive === true; | |
var indexOfMatch = caseSensitive ? value.indexOf(this.query) : value.toLowerCase().indexOf(that.query.toLowerCase()); | |
if(indexOfMatch != -1){ | |
var match = value.substr(indexOfMatch, that.query.length); | |
TD.innerHTML = value.replace(match, '<strong>' + match + '</strong>'); | |
} | |
} | |
}); | |
onBeforeKeyDownInner = function (event) { | |
var instance = this; | |
if (event.keyCode == Handsontable.helper.keyCode.ARROW_UP){ | |
if (instance.getSelected() && instance.getSelected()[0] === 0){ | |
that.instance.listen(); | |
that.focus(); | |
event.preventDefault(); | |
event.stopImmediatePropagation(); | |
} | |
} | |
}; | |
choicesListHot.addHook('beforeKeyDown', onBeforeKeyDownInner); | |
this.queryChoices(this.TEXTAREA.value); | |
}; | |
KeyValueAutocompleteEditor.prototype.close = function () { | |
this.$htContainer.handsontable('getInstance').removeHook('beforeKeyDown', onBeforeKeyDownInner); | |
// Clear hint if we're not going to have one | |
if(!this.$htContainer.handsontable('getInstance').getSelected()) { | |
this.saveValue([[null]]); | |
this.instance.getDataAtRow(this.row)[this.hintProp] = {}; | |
} | |
Handsontable.editors.HandsontableEditor.prototype.close.apply(this, arguments); | |
}; | |
KeyValueAutocompleteEditor.prototype.queryChoices = function(query){ | |
this.query = query; | |
if (typeof this.cellProperties.source == 'function'){ | |
var that = this; | |
this.cellProperties.source(query, function(choices){ | |
that.updateChoicesList(choices); | |
}); | |
} else if (Handsontable.helper.isArray(this.cellProperties.source)) { | |
this.updateChoicesList(this.cellProperties.source); | |
} else { | |
this.updateChoicesList([]); | |
} | |
}; | |
KeyValueAutocompleteEditor.prototype.updateChoicesList = function (choices) { | |
this.choices = choices; | |
this.$htContainer.handsontable('loadData', choices); | |
this.highlightBestMatchingChoice(); | |
this.focus(); | |
}; | |
KeyValueAutocompleteEditor.prototype.highlightBestMatchingChoice = function () { | |
var bestMatchingChoice = this.findBestMatchingChoice(); | |
if ( typeof bestMatchingChoice == 'undefined' && this.cellProperties.allowInvalid === false){ | |
bestMatchingChoice = 0; | |
} | |
if(typeof bestMatchingChoice == 'undefined'){ | |
this.$htContainer.handsontable('deselectCell'); | |
} else { | |
var endCol = this.$htContainer.handsontable('countCols') - 1; | |
this.$htContainer.handsontable('selectCell', bestMatchingChoice, 0, bestMatchingChoice, endCol, true); | |
} | |
}; | |
KeyValueAutocompleteEditor.prototype.findBestMatchingChoice = function(){ | |
var bestMatch = {}, | |
hot = this.$htContainer.handsontable('getInstance'), | |
valueLength = this.getValue().length, | |
currentItem, | |
indexOfValue, | |
charsLeft; | |
for(var i = 0, len = this.choices.length; i < len; i++){ | |
currentItem = this.cellProperties.extractTitle(this.choices[i]); | |
if(valueLength > 0){ | |
indexOfValue = currentItem.indexOf(this.getValue()); | |
} else { | |
indexOfValue = currentItem === this.getValue() ? 0 : -1; | |
} | |
if(indexOfValue == -1) continue; | |
charsLeft = currentItem.length - indexOfValue - valueLength; | |
if( typeof bestMatch.indexOfValue == 'undefined' || bestMatch.indexOfValue > indexOfValue || | |
( bestMatch.indexOfValue == indexOfValue && bestMatch.charsLeft > charsLeft ) ){ | |
bestMatch.indexOfValue = indexOfValue; | |
bestMatch.charsLeft = charsLeft; | |
bestMatch.index = i; | |
} | |
} | |
return bestMatch.index; | |
}; | |
KeyValueAutocompleteEditor.prototype.finishEditing = function (isCancelled, ctrlDown) { | |
var hot = this.$htContainer.handsontable('getInstance'), | |
selection = hot.getSelected(); | |
if (hot.isListening()) { //if focus is still in the HOT editor | |
this.instance.listen(); //return the focus to the parent HOT instance | |
} | |
if (selection) { | |
var selectedRow = selection[0], | |
value = this.cellProperties.extractValue(hot.getDataAtRow(selectedRow)), | |
title = this.cellProperties.extractTitle(hot.getDataAtRow(selectedRow)); | |
// Value stored in the table | |
if (value !== void 0) { | |
this.saveValue([[value]]); | |
// Store rest of data in a hint for future rendering | |
this.instance.getDataAtRow(this.row)[this.hintProp] = hot.getDataAtRow(selectedRow); | |
} | |
// Value of the editor text box | |
if (title !== void 0) { | |
this.setValue(title); | |
} | |
} | |
return Handsontable.editors.TextEditor.prototype.finishEditing.apply(this, arguments); | |
}; | |
return KeyValueAutocompleteEditor; | |
})(Handsontable); | |
// Editor (sorta) allowing an overlay with further details to be popped up. See | |
// DetailsListCell below. | |
var DetailsListEditor = (function (Handsontable) { | |
var DetailsListEditor = Handsontable.editors.TextEditor.prototype.extend(); | |
DetailsListEditor.prototype.beginEditing = function () { | |
var value = this.originalValue; | |
if(_.isNull(value)) { // turn null into a list | |
var rowData = this.instance.getData()[this.row]; | |
value = rowData[this.prop] = []; | |
} | |
this.cellProperties.showEditor(this.instance, this.TD, this.row, this.col, this.prop, value, this.cellProperties); | |
}; | |
DetailsListEditor.prototype.finishEditing = function () { | |
}; | |
DetailsListEditor.prototype.init = function () { | |
}; | |
DetailsListEditor.prototype.open = function () {}; | |
DetailsListEditor.prototype.close = function () {}; | |
DetailsListEditor.prototype.getValue = function () {}; | |
DetailsListEditor.prototype.setValue = function () {}; | |
DetailsListEditor.prototype.focus = function () {}; | |
return DetailsListEditor; | |
})(Handsontable); | |
return { | |
/** | |
* Standard table config | |
*/ | |
handsontableConfig: { | |
contextMenu: ['row_above', 'row_below', 'remove_row', 'hsep1', 'undo', 'redo'], | |
minSpareRows: 1, | |
rowHeaders: false, | |
autoWrapRow: true | |
}, | |
/** | |
* Turn a collection into a data structure suitable for Handsontable. | |
* If `modifier` is passed, it should be a function passed an attributes | |
* hash and an item in the collection, which may modifiy the former. | |
*/ | |
prepareTableData: function(collection, modifier) { | |
var data = []; | |
if (collection) { | |
collection.each(function(item, index, collection) { | |
var attrs = _.clone(item.attributes); | |
attrs.id = item.id; | |
attrs.cid = item.cid; | |
if(modifier) | |
modifier(attrs, item); | |
data.push(attrs); | |
}); | |
} | |
return data; | |
}, | |
/** | |
* Return a copy of the given handsontable's data with any spare rows | |
* sliced off. | |
*/ | |
getTableData: function(table) { | |
var minSpareRows = table.getSettings().minSpareRows; | |
if(minSpareRows > 0) { | |
return table.getData().slice(0, -minSpareRows); | |
} else { | |
return table.getData().slice(); | |
} | |
}, | |
/** | |
* Turn a list of handsontable data into a set of models for the given | |
* collection. Returns a promise. Success callbacks are called with | |
* a list of models. Failure callbacks are called with a list of | |
* parsed models, and a list of failed row indexes for the remaining | |
* rows. | |
* | |
* Note: If tableData contains a blank row due to minSpareRows, you | |
* should splice it before calling this function. | |
* | |
* Pass a function parseRow() to turn a row object into a set of | |
* properties appropriate for the collection's model. May optionally | |
* take the current/new, but unchanged, model as a second argument. | |
* Return false to indicate an error. | |
*/ | |
processTableData: function(tableData, collection, parseRow) { | |
var dfd = $.Deferred(), | |
models = [], | |
failed = [], | |
// Loop variables | |
row = null, | |
parsed = null, | |
cid = null, | |
model = null, | |
valid = true, | |
rowIndex = null; | |
function triggerInvalid() { | |
valid = false; | |
failed.push(rowIndex); | |
} | |
for(rowIndex = 0; rowIndex < tableData.length; ++rowIndex) { | |
row = tableData[rowIndex]; | |
cid = row.cid; | |
model = null; | |
valid = true; // reset each time | |
if(cid) model = collection.get(cid); | |
if(!model) model = new collection.model(); | |
parsed = parseRow? parseRow(row, model) : row; | |
if(parsed === false) { | |
triggerInvalid(); | |
} else { | |
model.once('invalid', triggerInvalid); | |
model.set(parsed, { | |
silent: true, | |
validate: true | |
}); | |
} | |
if(!valid) | |
continue; | |
models.push(model); | |
} | |
if(failed.length > 0) { | |
dfd.reject(models, failed); | |
} else { | |
dfd.resolve(models); | |
} | |
return dfd.promise(); | |
}, | |
/** | |
* Update a collection with new models. Any item that is removed | |
* will be destroyed. | |
*/ | |
updateCollection: function(collection, newModels) { | |
function destroyOnRemove(model) { | |
model.destroy(); | |
} | |
collection.on('remove', destroyOnRemove); | |
collection.set(newModels); | |
collection.off('remove', destroyOnRemove); | |
}, | |
/** | |
* Sync a single collection in its entirety as a single `update` | |
* request | |
*/ | |
syncCollection: function(collection, options) { | |
options = options ? _.clone(options) : {}; | |
if (options.parse === void 0) | |
options.parse = true; | |
var success = options.success; | |
options.success = function(resp) { | |
collection.reset(resp, options); | |
if (success) | |
success(collection, resp, options); | |
collection.trigger('sync', collection, resp, options); | |
}; | |
return collection.sync('update', collection, options); | |
}, | |
/** | |
* Return a promise that will be resolved if the table is valid, or | |
* rejected if invalid. When invalid, an array of invalid row/col | |
* coordinate pairs is provided. | |
*/ | |
validateTable: function(table) { | |
var deferred = new $.Deferred(), | |
waitingFor = [], | |
invalid = []; | |
function validatorChecker(validatorDeferred, invalid, i, j) { | |
return function(isValid) { | |
if(!isValid) { | |
invalid.push([i, j]); | |
} | |
validatorDeferred.resolve(); | |
}; | |
} | |
for(var i = 0; i < table.countRows() - table.getSettings().minSpareRows; ++i) { | |
for(var j = 0; j < table.countCols() - table.getSettings().minSpareCols; ++j) { | |
var validatorDeferred = new $.Deferred(); | |
waitingFor.push(validatorDeferred); | |
table.validateCell( | |
table.getDataAtCell(i, j), | |
table.getCellMeta(i, j), | |
validatorChecker(validatorDeferred, invalid, i, j), | |
'validateCells' | |
); | |
} | |
} | |
$.when.apply($, waitingFor) | |
.done(function() { | |
if(invalid.length > 0) { | |
invalid.forEach(function(coords) { | |
var node = table.getCell(coords[0], coords[1]); | |
if(node !== null) { | |
$(node).addClass(table.getSettings().invalidCellClassName); | |
} | |
}); | |
deferred.reject(invalid); | |
} else { | |
deferred.resolve(); | |
} | |
}) | |
.fail(function() { | |
deferred.reject(invalid); | |
}); | |
return deferred.promise(); | |
}, | |
/** | |
* Splice the data from newList into list, replacing any elements there already, | |
* in such a way that list remains the relevant reference | |
*/ | |
spliceTableData: function(table, list, newList) { | |
var minSpareRows = table.getSettings().minSpareRows; | |
newList.splice(-minSpareRows, minSpareRows); | |
Array.prototype.splice.apply(list, [0, list.length].concat(newList)); | |
}, | |
// Validators | |
validators: { | |
/** | |
* Validate that a value is provided | |
*/ | |
required: entryValidator(function(value, callback) { | |
if(value === null || value === undefined || value === "") { | |
callback(false); | |
} else if(_.isDate(value)) { | |
callback(true); | |
} else if(_.isArray(value) && _.isEmpty(value)) { | |
callback(false); | |
} else if(_.isObject(value) && _.isEmpty(value)) { | |
callback(false); | |
} else { | |
callback(true); | |
} | |
}), | |
/** | |
* Validate that a value is a number | |
*/ | |
number: entryValidator(function(value, callback) { | |
if(value === null || value === undefined || value === "") { | |
callback(true); | |
} else if(!_.isFinite(value)) { | |
callback(false); | |
} else { | |
callback(true); | |
} | |
}), | |
/** | |
* Validate that a value is a number and is provided | |
*/ | |
requiredNumber: entryValidator(function(value, callback) { | |
if(!_.isFinite(value)) { | |
callback(false); | |
} else { | |
callback(true); | |
} | |
}), | |
/** | |
* Validate that a value is a percentage | |
*/ | |
percentage: entryValidator(function(value, callback) { | |
if(value === null || value === undefined || value === "") { | |
callback(true); | |
} else if(!_.isFinite(value) || value < 0 || value > 100) { | |
callback(false); | |
} else { | |
callback(true); | |
} | |
}), | |
/** | |
* Validate that a value is a percentage and is provided | |
*/ | |
requiredPercentage: entryValidator(function(value, callback) { | |
if(!_.isFinite(value) || value < 0 || value > 100) { | |
callback(false); | |
} else { | |
callback(true); | |
} | |
}), | |
/** | |
* Validate that a value is a date | |
*/ | |
date: entryValidator(function(value, callback) { | |
if(value === null || value === undefined) { | |
callback(true); | |
} else if(!_.isDate(value)) { | |
callback(false); | |
} else { | |
callback(true); | |
} | |
}), | |
/** | |
* Validate that a value is a date and is provided | |
*/ | |
requiredDate: entryValidator(function(value, callback) { | |
if(!_.isDate(value)) { | |
callback(false); | |
} else { | |
callback(true); | |
} | |
}), | |
/** | |
* Validate that a value is unique in its column | |
*/ | |
unique: entryValidator(function(value, callback) { | |
var table = this.instance, | |
numRows = table.countRows() - table.getSettings().minSpareRows, | |
row = this.row, | |
col = this.col; | |
if(value === null || value === undefined || value === "") { | |
callback(false); | |
return; | |
} else if(_.isArray(value) && _.isEmpty(value)) { | |
callback(false); | |
return; | |
} else if(_.isObject(value) && _.isEmpty(value)) { | |
callback(false); | |
return; | |
} | |
for(var i = 0; i < numRows; ++i) { | |
if(i === row) { | |
continue; | |
} | |
var otherValue = table.getDataAtCell(i, col); | |
if(_.isEqual(value, otherValue)) { | |
callback(false); | |
return; | |
} | |
} | |
callback(true); | |
}) | |
}, | |
// Custom cell types | |
cellTypes: { | |
/** | |
* Date using Bootstrap datepicker | |
*/ | |
DateCell: { | |
renderer: function(instance, td, row, col, prop, value, cellProperties) { | |
if(_.isDate(value)) { | |
value = moment.utc(value).format(app.momentDateFormat); | |
} | |
Handsontable.AutocompleteRenderer(instance, td, row, col, prop, value, cellProperties); | |
}, | |
editor: DateEditor, | |
dataType: 'date' | |
}, | |
/** | |
* Percentage | |
*/ | |
PercentageCell: { | |
renderer: function(instance, td, row, col, prop, value, cellProperties) { | |
// Minor hack - we store percentages as whole numbers, not fractions, | |
// but the numeral library works in fractions | |
if(value) { | |
value /= 100; | |
} | |
Handsontable.NumericRenderer(instance, td, row, col, prop, value, cellProperties); | |
}, | |
editor: Handsontable.NumericCell.editor, | |
dataType: 'number' | |
}, | |
/** | |
* A list of values. Renders the number of items in the list. On edit, | |
* calls a callback function with the list, which can be edited inplace. | |
* For example: | |
* | |
* columns: [ | |
* { | |
* data: "allocations", | |
* type: TablePlugins.ChildListCell, | |
* showEditor: function(instance, td, row, col, prop, cellProperties, value) { | |
* ... | |
* } | |
* }, | |
* ... | |
* ] | |
* | |
* If the current value is null, a new list will be created | |
*/ | |
DetailsListCell: { | |
renderer: function(instance, td, row, col, prop, value, cellProperties) { | |
value = _.isNull(value)? "" : value.length + "..."; | |
Handsontable.AutocompleteRenderer(instance, td, row, col, prop, value, cellProperties); | |
}, | |
editor: DetailsListEditor | |
}, | |
/** | |
* See `KeyValueAutocompleteEditor` docs | |
*/ | |
KeyValueCell: { | |
renderer: function(instance, td, row, col, prop, value, cellProperties) { | |
if(value !== undefined && value !== null) { | |
var hint = instance.getDataAtRow(row)['_' + prop]; | |
if(hint !== undefined) { | |
value = cellProperties.extractTitle(hint); | |
} | |
} | |
Handsontable.AutocompleteRenderer(instance, td, row, col, prop, value, cellProperties); | |
}, | |
editor: KeyValueAutocompleteEditor | |
} | |
} | |
}; | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment