blob: b5c20432ce0fce1524bd906a30006304f6533c3c [file] [log] [blame]
/**
* Context aware drop down menu
* for annotation related query hints.
*
* @author Nils Diewald
*/
// - http://www.cryer.co.uk/resources/javascript/script20_respond_to_keypress.htm
// - https://developers.google.com/closure/compiler/docs/js-for-compiler
// TODO:
// - Add help option that opens the tutorial, e.g. to the foundry
// - http://en.wikipedia.org/wiki/JSDoc
// The last entry types (foundry, foundry/layer) is remembered and chosen by default
// Show the context at the top as breadcrumbs
// Highlight the context in the query (probably)
// Support backspace for removing the last prefix
/*
Alternative: Use right arrow for temporary context switch and arrow back
for temporary context removal
*/
/**
* The KorAP namespace for project related scripts
* @namespace
*/
var KorAP = KorAP || {};
/*
this._search.addEventListener(
"keyup",
function () {
that.update();
},
false
);
*/
(function (KorAP) {
"use strict";
// Don't let events bubble up
if (Event.halt === undefined) {
Event.prototype.halt = function () {
this.stopPropagation();
this.preventDefault();
};
};
// Default log message
KorAP.log = KorAP.log || function (type, msg) {
console.log(type + ": " + msg);
};
/* TODO: Add event listener on windows resize to change that! */
/** @define {number} Limited view of menu items */
KorAP.limit = 8;
/**
* @define {regex} Regular expression for context
*/
KorAP.context =
"(?:^|[^-_a-zA-Z0-9])" + // Anchor
"((?:[-_a-zA-Z0-9]+?)\/" + // Foundry
"(?:" +
"(?:[-_a-zA-Z0-9]+?)=" + // Layer
"(?:(?:[^:=\/ ]+?):)?" + // Key
")?" +
")$";
// Initialize hint array
KorAP.hintArray = KorAP.hintArray || {};
KorAP.updateKeyDown = function (event) {
};
KorAP.InputField = {
create : function (element) {
return Object.create(KorAP.InputField)._init(element);
},
_init : function (element) {
this._element = element;
// Create mirror for searchField
if ((this._mirror = document.getElementById("searchMirror")) === null) {
this._mirror = document.createElement("div");
this._mirror.setAttribute("id", "searchMirror");
this._mirror.appendChild(document.createElement("span"));
this._mirror.style.height = "1px";
document.getElementsByTagName("body")[0].appendChild(this._mirror);
};
// Update position of the mirror
var that = this;
window.resize = function () {
that.reposition();
};
/*
// Add event listener for key down
element.addEventListener(
"keydown",
function (e) {
// KorAP.updateKeyDown(e).bind(that)
},
false
);
*/
return this;
},
get mirror () {
return this._mirror;
},
get element () {
return this._element;
},
get value () {
return this._element.value;
},
update : function () {
this._mirror.firstChild.textContent = this.split()[0];
},
insert : function (text) {
var splittedText = this.split();
var s = this.element;
s.value = splittedText[0] + text + splittedText[1];
s.selectionStart = (splittedText[0] + text).length;
s.selectionEnd = s.selectionStart;
this._mirror.firstChild.textContent = splittedText[0] + text;
},
// Return two substrings, splitted at current position
split : function () {
var s = this._element;
var value = s.value;
var start = s.selectionStart;
return new Array(
value.substring(0, start),
value.substring(start, value.length)
);
},
// Position the input mirror directly below the input box
reposition : function () {
var inputClientRect = this._element.getBoundingClientRect();
var inputStyle = window.getComputedStyle(this._element, null);
var mirrorStyle = this._mirror.style;
mirrorStyle.left = inputClientRect.left + "px";
mirrorStyle.top = inputClientRect.bottom + "px";
// These may be relevant in case of media depending css
mirrorStyle.paddingLeft = inputStyle.getPropertyValue("padding-left");
mirrorStyle.marginLeft = inputStyle.getPropertyValue("margin-left");
mirrorStyle.borderLeftWidth = inputStyle.getPropertyValue("border-left-width");
mirrorStyle.borderLeftStyle = inputStyle.getPropertyValue("border-left-style");
mirrorStyle.fontSize = inputStyle.getPropertyValue("font-size");
mirrorStyle.fontFamily = inputStyle.getPropertyValue("font-family");
},
get context () {
return this.split()[0];
}
};
KorAP.Hint = {
_firstTry : true,
create : function (param) {
return Object.create(KorAP.Hint)._init(param);
},
_init : function (param) {
param = param || {};
this._menu = {};
// Get input field
this._inputField = KorAP.InputField.create(
param["inputField"] || document.getElementById("q-field")
);
var that = this;
var inputFieldElement = this._inputField.element;
// Add event listener for key pressed down
inputFieldElement.addEventListener(
"keypress", function (e) {that.updateKeyPress(e)}, false
);
// Set Analyzer for context
this._analyzer = KorAP.ContextAnalyzer.create(
param["context"]|| KorAP.context
);
return this;
},
_codeFromEvent : function (e) {
if ((e.charCode) && (e.keyCode==0))
return e.charCode
return e.keyCode;
},
updateKeyPress : function (e) {
if (!this._active)
return;
var character = String.fromCharCode(
this._codeFromEvent(e)
);
e.halt(); // No event propagation
console.log("TODO: filter view");
},
updateKeyDown : function (e) {
var code = this._codeFromEvent(e)
/*
* keyCodes:
* - Down = 40
* - Esc = 27
* - Up = 38
* - Enter = 13
* - shift = 16
* for characters use e.key
*/
switch (code) {
case 27: // 'Esc'
// TODO: menu.hide();
break;
case 40: // 'Down'
e.halt(); // No event propagation
// Menu is not active
if (!this._active)
this.popUp();
// Menu is active
else {
// TODO: that.removePrefix();
// TODO: menu.next();
};
break;
case 38: // "Up"
if (!this._active)
break;
e.halt(); // No event propagation
// TODO: that.removePrefix();
// TODO: menu.prev();
break;
case 13: // "Enter"
if (!this._active)
break;
e.halt(); // No event propagation
// TODO: that.insertText(menu.getActiveItem().getAction());
// TODO: that.removePrefix();
// Remove menu
// TODO: menu.hide();
// Fill this with the correct value
// Todo: This is redundant with click function
/*
var show;
if ((show = that.analyzeContext()) != "-") {
menu.show(show);
menu.update(
e.target.getBoundingClientRect().right
);
};
*/
break;
default:
if (!this._active)
return;
// Surpress propagation in firefox
/*
if (e.key !== undefined && e.key.length != 1) {
menu.hide();
};
*/
};
},
getMenuByContext : function () {
var context = this._inputField.context;
if (context === undefined || context.length == 0)
return this.menu("-");
context = this._analyzer.analyze(context);
if (context === undefined || context.length == 0)
return this.menu("-");
return this.menu(context);
},
// Return and probably init a menu based on an action
menu : function (action) {
if (this._menu[action] === undefined) {
if (KorAP.hintArray[action] === undefined)
return;
this._menu[action] = KorAP.menu.create(action, KorAP.hintArray[action]);
};
return this._menu[action];
},
get inputField () {
return this._inputField;
},
get active () {
return this._active;
},
popUp : function () {
if (this.active)
return;
if (this._firstTry) {
this._inputField.reposition();
this._firstTry = false;
};
// update
var menu;
if (menu = this.getMenuByContext()) {
menu.show();
// Update bounding box
}
else {
// this.hide();
};
// Focus on input field
this.inputField.element.focus();
}
};
/**
Regex object for checking the context of the hint
*/
KorAP.ContextAnalyzer = {
create : function (regex) {
return Object.create(KorAP.ContextAnalyzer)._init(regex);
},
_init : function (regex) {
try {
this._regex = new RegExp(regex);
}
catch (e) {
KorAP.log("error", e);
return;
};
return this;
},
test : function (text) {
if (!this._regex.exec(text))
return;
return RegExp.$1;
}
};
/**
* List of items for drop down menu (complete).
* Only a sublist of the menu is filtered (live).
* Only a sublist of the filtered menu is visible (shown).
*/
KorAP.Menu = {
_position : 0, // position in the active list
_active : -1, // active item in the item list
/**
* Create new Menu based on the action prefix
* and a list of menu items.
*
* @this {Menu}
* @constructor
* @param {string} Context prefix
* @param {Array.<Array.<string>>} List of menu items
*/
create : function (context, items) {
return Object.create(KorAP.Menu)._init(context, items);
},
/*
* Make the previous item in the menu active
*/
prev : function () {
if (this._position == -1)
return;
// Set new live item
var oldItem = this.liveItem(this._position--);
oldItem.active(false);
var newItem = this.liveItem(this._position);
// The previous element is undefined - roll to bottom
if (newItem === undefined) {
this._position = this.liveLength - 1;
newItem = this.liveItem(this._position);
this._offset = this.liveLength - this.limit;
this._showItems(this._offset);
}
// The previous element is outside the view - roll up
else if (this._position < this._offset) {
this._removeLast();
this._offset--;
this._prepend(this._list[this._position]);
};
newItem.active(true);
},
/*
* Make the next item in the menu active
*/
next : function () {
// No active element set
if (this._position == -1)
return;
// Set new live item
var oldItem = this.liveItem(this._position++);
oldItem.active(false);
var newItem = this.liveItem(this._position);
// The next element is undefined - roll to top
if (newItem === undefined) {
this._offset = 0;
this._position = 0;
newItem = this.liveItem(0);
this._showItems(0);
}
// The next element is outside the view - roll down
else if (this._position >= (this.limit + this._offset)) {
this._removeFirst();
this._offset++;
this._append(this._list[this._position]);
};
newItem.active(true);
},
/**
* Delete all visible items from the menu element
*/
delete : function () {
var child;
for (var i = 0; i <= this.limit; i++)
if (child = this.shownItem(i))
child.lowlight();
while (child = this._element.firstChild)
this._element.removeChild(child);
},
/**
* Filter the list and make it visible
*
* @param {string} Prefix for filtering the list
*/
show : function (prefix) {
this._prefix = prefix;
// Initialize the list
if (!this._initList())
return;
// show based on offset
this._showItems(0);
// Set the first element to active
this.liveItem(0).active(true);
this._position = 0;
this._active = this._list[0];
// Add classes for rolling menus
this._boundary(true);
},
/**
* Get the prefix for filtering,
* e.g. &quot;ve"&quot; for &quot;verb&quot;
*/
get prefix () {
return this._prefix || '';
},
/**
* Get the numerical value for limit
*/
get limit () {
return KorAP.limit;
},
/**
* Get the context of the menue,
* e.g. &quot;tt/&quot; for the tree tagger menu
*/
get context () {
return this._context;
},
/**
* Get a specific item from the complete list
*
* @param {number} index of the list item
*/
item : function (index) {
return this._items[index]
},
/**
* Get a specific item from the filtered list
*
* @param {number} index of the list item
*/
liveItem : function (index) {
if (this._list === undefined)
if (!this._initList())
return;
return this._items[this._list[index]];
},
/*
* Get a specific item from the visible list
*
* @param {number} index of the list item
*/
shownItem : function (index) {
if (index >= this.limit)
return;
return this.liveItem(this._offset + index);
},
get element () {
return this._element;
},
get length () {
return this._items.length;
},
get liveLength () {
if (this._list === undefined)
this._initList();
return this._list.length;
},
chooseHint : function (e) {
/*
var element = e.target;
while (element.nodeName == "STRONG" || element.nodeName == "SPAN")
element = element.parentNode;
if (element === undefined || element.nodeName != "LI")
return;
var action = element.getAttribute('data-action');
hint.insertText(action);
var menu = hint.menu();
menu.hide();
// Fill this with the correct value
var show;
if ((show = hint.analyzeContext()) != "-") {
menu.show(show);
menu.update(
hint._search.getBoundingClientRect().right
);
};
hint._search.focus();
*/
},
_reset : function () {
this._offset = 0;
this._pos = 0;
this._prefix = undefined;
},
_boundary : function (bool) {
this.item(this._list[0]).noMore(bool);
this.item(this._list[this._list.length - 1]).noMore(bool);
},
_initList : function () {
console.log("..." + this._items.length);
if (this._list === undefined)
this._list = [];
else if (this._list.length != 0) {
this._boundary(false);
this._list.length = 0;
};
this._offset = 0;
if (this.prefix().length <= 0) {
for (var i = 0; i < this._items.length; i++)
this._list.push(i);
return true;
};
var pos;
var paddedPrefix = " " + this.prefix;
for (pos = 0; pos < this._items.length; pos++) {
if ((this.item(pos).lcfield.indexOf(paddedPrefix)) >= 0)
this._list.push(pos);
};
if (this._list.length == 0) {
for (pos = 0; pos < this._items.length; pos++) {
if ((this.item(pos).lcfield.indexOf(this.prefix)) >= 0)
this._list.push(pos);
};
};
// Filter was successful
return this._list.length > 0 ? true : false;
},
_removeFirst : function () {
this.item(this._list[this._offset]).lowlight();
this._element.removeChild(this._element.firstChild);
},
_removeLast : function () {
this.item(this._list[this._offset + this.limit - 1]).lowlight();
this._element.removeChild(this._element.lastChild);
},
// Append item to the shown list based on index
_append : function (i) {
var item = this.item(i);
// Highlight based on prefix
if (this.prefix.length > 0)
item.highlight(this.prefix);
// Append element
this.element.appendChild(item.element);
},
// Prepend item to the shown list based on index
_prepend : function (i) {
var item = this.item(i);
// Highlight based on prefix
if (this.prefix.length > 0)
item.highlight(this.prefix);
// Append element
this.element.insertBefore(
item.element,
this.element.firstChild
);
},
_init : function (context, items) {
this._context = context;
this._element = document.createElement("ul");
this._element.style.opacity = 0;
this.active = false;
/*
Todo:
this._element.addEventListener("click", chooseHint, false);
*/
this._items = new Array();
var i;
for (i in items)
this._items.push(KorAP.MenuItem.create(items[i]));
this._reset();
return this;
},
_showItems : function (offset) {
this.delete();
// Use list
var shown = 0;
var i;
for (i in this._list) {
// Don't show - it's before offset
if (shown++ < offset)
continue;
this._append(this._list[i]);
if (shown >= (this.limit + this._offset))
break;
};
}
};
/**
* Item in the Dropdown menu
*/
KorAP.MenuItem = {
/**
* Create a new MenuItem object.
*
* @constructor
* @this {MenuItem}
* @param {Array.<string>} An array object of name, action and
* optionally a description
*/
create : function (params) {
return Object.create(KorAP.MenuItem)._init(params);
},
/**
* Get the name of the item
*/
get name () {
return this._name;
},
/**
* Get the action string
*/
get action () {
return this._action;
},
/**
* Get the description of the item
*/
get desc () {
return this._desc;
},
/**
* Get the lower case field
*/
get lcfield () {
return this._lcfield;
},
/**
* Check or set if the item is active
*
* @param {boolean|null} State of activity
*/
active : function (bool) {
var cl = this.element.classList;
if (bool === undefined)
return cl.contains("active");
else if (bool)
cl.add("active");
else
cl.remove("active");
},
/**
* Check or set if the item is
* at the boundary of the menu
* list
*
* @param {boolean|null} State of activity
*/
noMore : function (bool) {
var cl = this.element.classList;
if (bool === undefined)
return cl.contains("no-more");
else if (bool)
cl.add("no-more");
else
cl.remove("no-more");
},
/**
* Get the document element of the menu item
*/
get element () {
// already defined
if (this._element !== undefined)
return this._element;
// Create list item
var li = document.createElement("li");
li.setAttribute("data-action", this._action);
// Create title
var name = document.createElement("strong");
name.appendChild(document.createTextNode(this._name));
li.appendChild(name);
// Create description
if (this._desc !== undefined) {
var desc = document.createElement("span");
desc.appendChild(document.createTextNode(this._desc));
li.appendChild(desc);
};
return this._element = li;
},
/**
* Highlight parts of the item
*
* @param {string} Prefix string for highlights
*/
highlight : function (prefix) {
var e = this.element;
this._highlight(e.firstChild, prefix);
if (this._desc !== undefined)
this._highlight(e.lastChild, prefix);
},
/**
* Remove highlight of the menu item
*/
lowlight : function () {
var e = this.element;
e.firstChild.innerHTML = this._name;
if (this._desc !== undefined)
e.lastChild.innerHTML = this._desc;
},
// Initialize menu item
_init : function (params) {
if (params[0] === undefined || params[1] === undefined)
throw new Error("Missing parameters");
this._name = params[0];
this._action = params[1];
this._lcfield = " " + this._name.toLowerCase();
if (params.length > 2) {
this._desc = params[2];
this._lcfield += " " + this._desc.toLowerCase();
};
return this;
},
// Highlight a certain element of the menu item
_highlight : function (elem, prefix) {
var text = elem.firstChild.nodeValue;
var textlc = text.toLowerCase();
var pos = textlc.indexOf(prefix);
if (pos >= 0) {
// First element
elem.firstChild.nodeValue = pos > 0 ? text.substr(0, pos) : "";
// Second element
var hl = document.createElement("em");
hl.appendChild(
document.createTextNode(text.substr(pos, prefix.length))
);
elem.appendChild(hl);
// Third element
elem.appendChild(
document.createTextNode(text.substr(pos + prefix.length))
);
};
}
};
}(this.KorAP));