Skip to content

Instantly share code, notes, and snippets.

@optilude
Last active December 2, 2015 13:07
Show Gist options
  • Save optilude/9864800 to your computer and use it in GitHub Desktop.
Save optilude/9864800 to your computer and use it in GitHub Desktop.
/**
* 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;
};
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,
keyTimeout = 250,
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());
}, 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);
@radusl
Copy link

radusl commented Dec 31, 2014

i tried your example and i get an error "Uncaught ReferenceError: _ is not defined" at KeyValueAutocompleteEditor.js at line 164

@sarigue
Copy link

sarigue commented Dec 2, 2015

radusl :
you can replace
!_.isString(value)
by:
typeof(value)!='string'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment