New hint with specs
diff --git a/public/js/src/hint.js b/public/js/src/hint.js
new file mode 100644
index 0000000..3c2e3ca
--- /dev/null
+++ b/public/js/src/hint.js
@@ -0,0 +1,840 @@
+/**
+ * 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 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 = 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. "ve"" for "verb"
+ */
+ get prefix () {
+ return this._prefix || '';
+ },
+
+ /**
+ * Get the numerical value for limit
+ */
+ get limit () {
+ return KorAP.limit;
+ },
+
+ /**
+ * Get the context of the menue,
+ * e.g. "tt/" 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 () {
+ 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));