escher-vis
Version:
Escher: A Web Application for Building, Sharing, and Embedding Data-Rich Visualizations of Biological Pathways
417 lines (374 loc) • 16.3 kB
JavaScript
/**
* @license
*
* complete.ly 1.0.0
* MIT Licensing
* Copyright (c) 2013 Lorenzo Puccetti
*
* This Software shall be used for doing good things, not bad things.
*
*
* Modified by Zachary King (c) 2014.
*
**/
var utils = require('./utils');
module.exports = function(container, config) {
document = utils.get_document(container);
window = utils.get_window(container);
config = config || {};
config.fontSize = config.fontSize || '13px';
config.fontFamily = config.fontFamily || 'sans-serif';
config.promptInnerHTML = config.promptInnerHTML || '';
config.color = config.color || '#333';
config.hintColor = config.hintColor || '#aaa';
config.backgroundColor = config.backgroundColor || '#fff';
config.dropDownBorderColor = config.dropDownBorderColor || '#aaa';
config.dropDownZIndex = config.dropDownZIndex || '100'; // to ensure we are in front of everybody
config.dropDownOnHoverBackgroundColor = config.dropDownOnHoverBackgroundColor || '#ddd';
var txtInput = document.createElement('input');
txtInput.type ='text';
txtInput.spellcheck = false;
txtInput.style.fontSize = config.fontSize;
txtInput.style.fontFamily = config.fontFamily;
txtInput.style.color = config.color;
txtInput.style.backgroundColor = config.backgroundColor;
txtInput.style.width = '100%';
txtInput.style.outline = '0';
txtInput.style.border = '0';
txtInput.style.margin = '0';
txtInput.style.padding = '0';
var txtHint = txtInput.cloneNode();
txtHint.disabled='';
txtHint.style.position = 'absolute';
txtHint.style.top = '0';
txtHint.style.left = '0';
txtHint.style.borderColor = 'transparent';
txtHint.style.boxShadow = 'none';
txtHint.style.color = config.hintColor;
txtInput.style.backgroundColor ='transparent';
txtInput.style.verticalAlign = 'top';
txtInput.style.position = 'relative';
var wrapper = document.createElement('div');
wrapper.style.position = 'relative';
wrapper.style.outline = '0';
wrapper.style.border = '0';
wrapper.style.margin = '0';
wrapper.style.padding = '0';
var prompt = document.createElement('div');
prompt.style.position = 'absolute';
prompt.style.outline = '0';
prompt.style.margin = '0';
prompt.style.padding = '0';
prompt.style.border = '0';
prompt.style.fontSize = config.fontSize;
prompt.style.fontFamily = config.fontFamily;
prompt.style.color = config.color;
prompt.style.backgroundColor = config.backgroundColor;
prompt.style.top = '0';
prompt.style.left = '0';
prompt.style.overflow = 'hidden';
prompt.innerHTML = config.promptInnerHTML;
prompt.style.background = 'transparent';
if (document.body === undefined) {
throw 'document.body is undefined. The library was wired up incorrectly.';
}
document.body.appendChild(prompt);
var w = prompt.getBoundingClientRect().right; // works out the width of the prompt.
wrapper.appendChild(prompt);
prompt.style.visibility = 'visible';
prompt.style.left = '-'+w+'px';
wrapper.style.marginLeft= w+'px';
wrapper.appendChild(txtHint);
wrapper.appendChild(txtInput);
var dropDown = document.createElement('div');
dropDown.style.position = 'absolute';
dropDown.style.visibility = 'hidden';
dropDown.style.outline = '0';
dropDown.style.margin = '0';
dropDown.style.padding = '0';
dropDown.style.textAlign = 'left';
dropDown.style.fontSize = config.fontSize;
dropDown.style.fontFamily = config.fontFamily;
dropDown.style.backgroundColor = config.backgroundColor;
dropDown.style.zIndex = config.dropDownZIndex;
dropDown.style.cursor = 'default';
dropDown.style.borderStyle = 'solid';
dropDown.style.borderWidth = '1px';
dropDown.style.borderColor = config.dropDownBorderColor;
dropDown.style.overflowX= 'hidden';
dropDown.style.whiteSpace = 'pre';
dropDown.style.overflowY = 'scroll';
var createDropDownController = function(elem) {
var rows = [];
var ix = 0;
var oldIndex = -1;
var current_row = null;
var onMouseOver = function() { this.style.outline = '1px solid #ddd'; }
var onMouseOut = function() { this.style.outline = '0'; }
var onDblClick = function(e) {
e.preventDefault();
p.onmouseselection(this.id);
}
var p = {
hide : function() { elem.style.visibility = 'hidden'; },
refresh : function(token, options) {
elem.style.visibility = 'hidden';
ix = 0;
elem.innerHTML ='';
var vph = (window.innerHeight || document.documentElement.clientHeight);
var rect = elem.parentNode.getBoundingClientRect();
var distanceToTop = rect.top - 6; // heuristic give 6px
var distanceToBottom = vph - rect.bottom -6; // distance from the browser border.
rows = [];
for (var i = 0; i < options.length; i++) {
// ignore case
var found = options[i].matches.filter(function(match) {
return match.toLowerCase().indexOf(token.toLowerCase()) == 0;
});
if (found.length == 0)
continue;
var divRow = document.createElement('div');
divRow.style.color = config.color;
divRow.onmouseover = onMouseOver;
divRow.onmouseout = onMouseOut;
// prevent selection for double click
divRow.onmousedown = function(e) { e.preventDefault(); };
divRow.ondblclick = onDblClick;
divRow.__hint = found[0];
divRow.id = options[i].id;
divRow.innerHTML = options[i].html;
rows.push(divRow);
elem.appendChild(divRow);
// limit results and add a note at the buttom
if (rows.length >= rs.display_limit) {
var divRow2 = document.createElement('div');
divRow2.innerHTML = ' ' + (options.length - rows.length) + ' more';
rows.push(divRow2);
elem.appendChild(divRow2);
break;
}
}
if (rows.length===0) {
return; // nothing to show.
}
p.highlight(0);
// Heuristic (only when the distance to the to top is 4
// times more than distance to the bottom
if (distanceToTop > distanceToBottom*3) {
// we display the dropDown on the top of the input text
elem.style.maxHeight = distanceToTop+'px';
elem.style.top ='';
elem.style.bottom ='100%';
} else {
elem.style.top = '100%';
elem.style.bottom = '';
elem.style.maxHeight = distanceToBottom+'px';
}
elem.style.visibility = 'visible';
},
highlight : function(index) {
if (oldIndex !=-1 && rows[oldIndex]) {
rows[oldIndex].style.backgroundColor = config.backgroundColor;
}
rows[index].style.backgroundColor = config.dropDownOnHoverBackgroundColor; // <-- should be config
oldIndex = index;
current_row = rows[index];
},
// moves the selection either up or down (unless it's not
// possible) step is either +1 or -1.
move : function(step) {
// nothing to move if there is no dropDown. (this happens if
// the user hits escape and then down or up)
if (elem.style.visibility === 'hidden')
return '';
// No circular scrolling
if (ix+step === -1 || ix+step === rows.length)
return rows[ix].__hint;
ix+=step;
p.highlight(ix);
return rows[ix].__hint;
},
onmouseselection : function() {},
get_current_row: function () {
return current_row;
}
};
return p;
}
var dropDownController = createDropDownController(dropDown);
dropDownController.onmouseselection = function(id) {
rs.onEnter(id)
rs.input.focus();
}
wrapper.appendChild(dropDown);
container.appendChild(wrapper);
var spacer,
// This will contain the leftSide part of the textfield (the bit that
// was already autocompleted)
leftSide;
function calculateWidthForText(text) {
if (spacer === undefined) { // on first call only.
spacer = document.createElement('span');
spacer.style.visibility = 'hidden';
spacer.style.position = 'fixed';
spacer.style.outline = '0';
spacer.style.margin = '0';
spacer.style.padding = '0';
spacer.style.border = '0';
spacer.style.left = '0';
spacer.style.whiteSpace = 'pre';
spacer.style.fontSize = config.fontSize;
spacer.style.fontFamily = config.fontFamily;
spacer.style.fontWeight = 'normal';
document.body.appendChild(spacer);
}
// Used to encode an HTML string into a plain text.
// taken from http://stackoverflow.com/questions/1219860/javascript-jquery-html-encoding
spacer.innerHTML = String(text).replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '<')
.replace(/>/g, '>');
return spacer.getBoundingClientRect().right;
}
var rs = {
get_hint : function(x) { return x; },
display_limit: 1000,
onArrowDown : function() {}, // defaults to no action.
onArrowUp : function() {}, // defaults to no action.
onEnter : function() {}, // defaults to no action.
onTab : function() {}, // defaults to no action.
onChange: function() { rs.repaint() }, // defaults to repainting.
startFrom: 0,
options: [],
// Only to allow easy access to the HTML elements to the final user
// (possibly for minor customizations)
wrapper : wrapper,
input : txtInput,
hint : txtHint,
dropDown : dropDown,
prompt : prompt,
setText : function(text) {
txtHint.value = text;
txtInput.value = text;
},
getText : function() {
return txtInput.value;
},
hideDropDown : function() {
dropDownController.hide();
},
repaint : function() {
var text = txtInput.value;
var startFrom = rs.startFrom;
var options = rs.options;
var optionsLength = options.length;
// breaking text in leftSide and token.
var token = text.substring(startFrom);
leftSide = text.substring(0,startFrom);
// updating the hint.
txtHint.value ='';
for (var i = 0; i < optionsLength; i++) {
var found = options[i].matches.filter(function(match) {
return match.toLowerCase().indexOf(token.toLowerCase()) == 0;
});
if (found.length == 0)
continue;
txtHint.value = rs.get_hint(found[0]);
break;
}
// moving the dropDown and refreshing it.
dropDown.style.left = calculateWidthForText(leftSide)+'px';
dropDownController.refresh(token, rs.options);
}
};
var registerOnTextChangeOldValue;
// Register a callback function to detect changes to the content of the
// input-type-text. Those changes are typically followed by user's
// action: a key-stroke event but sometimes it might be a mouse click.
var registerOnTextChange = function(txt, callback) {
registerOnTextChangeOldValue = txt.value;
var handler = function() {
var value = txt.value;
if (registerOnTextChangeOldValue !== value) {
registerOnTextChangeOldValue = value;
callback(value);
}
};
// For user's actions, we listen to both input events and key up events
// It appears that input events are not enough so we defensively listen to key up events too.
// source: http://help.dottoro.com/ljhxklln.php
//
// The cost of listening to three sources should be negligible as the handler will invoke callback function
// only if the text.value was effectively changed.
txt.addEventListener("input", handler, false);
txt.addEventListener('keyup', handler, false);
txt.addEventListener('change', handler, false);
};
registerOnTextChange(txtInput,function(text) { // note the function needs to be wrapped as API-users will define their onChange
rs.onChange(text);
rs.repaint();
});
var keyDownHandler = function(e) {
e = e || window.event;
var keyCode = e.keyCode;
if (keyCode == 33) { return; } // page up (do nothing)
if (keyCode == 34) { return; } // page down (do nothing);
// right, end, tab (autocomplete triggered)
if (keyCode == 39 || keyCode == 35 || keyCode == 9) {
// for tabs we need to ensure that we override the default
// behaviour: move to the next focusable HTML-element
if (keyCode == 9) {
e.preventDefault();
e.stopPropagation();
if (txtHint.value.length == 0) {
// tab was called with no action.
rs.onTab();
}
}
// if there is a hint
if (txtHint.value.length > 0) {
txtInput.value = txtHint.value;
var hasTextChanged = registerOnTextChangeOldValue != txtInput.value
// avoid dropDown to appear again
registerOnTextChangeOldValue = txtInput.value;
// for example imagine the array contains the following
// words: bee, beef, beetroot. User has hit enter to get
// 'bee' it would be prompted with the dropDown again (as
// beef and beetroot also match)
if (hasTextChanged) {
// force it.
rs.onChange(txtInput.value);
}
}
return;
}
if (keyCode == 13) { // enter
// get current
var id = dropDownController.get_current_row().id;
rs.onEnter(id);
return;
}
if (keyCode == 40) { // down
var m = dropDownController.move(+1);
if (m == '') { rs.onArrowDown(); }
txtHint.value = rs.get_hint(m);
return;
}
if (keyCode == 38 ) { // up
var m = dropDownController.move(-1);
if (m == '') { rs.onArrowUp(); }
txtHint.value = rs.get_hint(m);
e.preventDefault();
e.stopPropagation();
return;
}
// it's important to reset the txtHint on key down. Think: user
// presses a letter (e.g. 'x') and never releases. You get
// (xxxxxxxxxxxxxxxxx) and you would see still the hint. Reset the
// txtHint. (it might be updated onKeyUp).
txtHint.value ='';
};
txtInput.addEventListener("keydown", keyDownHandler, false);
return rs;
};