diff --git a/zanata-war/src/main/resources/org/zanata/webtrans/public/Application.xhtml b/zanata-war/src/main/resources/org/zanata/webtrans/public/Application.xhtml
index 18c27ac3f6..ea31102ce1 100644
--- a/zanata-war/src/main/resources/org/zanata/webtrans/public/Application.xhtml
+++ b/zanata-war/src/main/resources/org/zanata/webtrans/public/Application.xhtml
@@ -34,6 +34,8 @@
+
+
diff --git a/zanata-war/src/main/resources/org/zanata/webtrans/public/search-field-suggestions.js b/zanata-war/src/main/resources/org/zanata/webtrans/public/search-field-suggestions.js
new file mode 100644
index 0000000000..576182107b
--- /dev/null
+++ b/zanata-war/src/main/resources/org/zanata/webtrans/public/search-field-suggestions.js
@@ -0,0 +1,239 @@
+
+window.searchSuggestions = (function () {
+
+ function init (wrapper, valueChangeCallback) {
+ var i;
+ var resultList = wrapper.querySelector('.js-suggest__results');
+ var waitingList = document.createElement('ul');
+ waitingList.className = 'is-invisible';
+ // insert waitingList after resultList
+ wrapper.insertBefore(waitingList, resultList.nextSibling);
+
+ var resultElements = resultList.querySelectorAll('.js-suggest__result');
+
+ for (i=0; i < resultElements.length; i++) {
+ waitingList.appendChild(resultElements[i]);
+ }
+
+ function selectTargetResult (result) {
+ clearResultSelection();
+ result.classList.add('is-selected');
+ }
+
+ function clearResultSelection () {
+ for (var i=0; i < resultElements.length; i++) {
+ resultElements[i].classList.remove('is-selected');
+ }
+ }
+
+ function insertTargetResult (result) {
+ result.classList.remove('is-selected');
+ insertKey(result.dataset.key);
+ }
+
+ var input = wrapper.querySelector('.js-suggest__input');
+
+ function insertKey (key) {
+ var currentCursor = input.selectionStart;
+ // FIXME adding the colon here doesn't seem right, seems too fragile. Find a more sensible place to add it.
+ var newValue = replaceTargetWord(input.value, currentCursor, key + ':');
+ var newCursor = getEndPosOfCursorWord (newValue, currentCursor);
+ updateQueryAndCursor(newValue, newCursor);
+ }
+
+ function updateQueryAndCursor(query, cursorPos) {
+ input.value = query;
+ input.setSelectionRange(cursorPos, cursorPos);
+ calculateSuggestions();
+ }
+
+ function attachResultEvents (element) {
+ element.addEventListener('mouseover', function () { selectTargetResult(element); });
+ element.addEventListener('click', function () { insertTargetResult(element); });
+ }
+
+ for (i=0; i < resultElements.length; i++) {
+ attachResultEvents(resultElements[i]);
+ }
+
+ var suggest = suggestFromElements(resultElements);
+
+ function calculateSuggestions () {
+ var ele,
+ suggested = suggest(getCursorWord(input.value, input.selectionStart));
+ showSuggestions();
+ // move non-matching onto waitinglist
+ for (var i=0; i < resultElements.length; i++) {
+ ele = resultElements[i];
+ if (suggested.indexOf(ele) === -1) {
+ ele.classList.remove('is-selected');
+ waitingList.appendChild(ele);
+ }
+ }
+ // move matching onto resultlist in order
+ for (i=0; i query.length) {
+ console.error("cursor is outside of string");
+ }
+
+ var left = query.slice(0, cursor),
+ right = query.slice(cursor);
+
+ // This will match only balanced strings (FIXME name better)
+ var isQuoted = /^(?:[^\\"]|\\.|"(?:[^\\"]|\\.)*?")*$/g.exec(left);
+ if (!isQuoted) {
+ return null; // no key contains null, so nothing is suggested in quoted sections. FIXME make other code check for null so the behaviour is obvious
+ }
+
+ var leftMatch = /^.*?(\S*)$/g.exec(left)[1];
+ if (leftMatch.length === 0) {
+ // in front of word, counts as no word
+ return '';
+ }
+
+ // join non-whitespace before the cursor to non-whitespace after the cursor
+ return leftMatch + /^([^:\s]*):?.*?$/g.exec(right)[1];
+ }
+
+ // replacement already has : on it
+ function replaceTargetWord (query, cursor, replacement) {
+ if (cursor < 0 || cursor > query.length) {
+ // if outside range, just append it
+ return query + replacement;
+ }
+
+ var left = query.slice(0, cursor),
+ leftMatches = /^(.*?)(\S*)$/g.exec(left),
+ right = query.slice(cursor);
+
+ if (leftMatches[2].length === 0) {
+ return left + replacement + right;
+ } else {
+ return leftMatches[1] + replacement + /^[^:\s]*:?(.*?)$/g.exec(right)[1];
+ }
+ }
+
+ function getEndPosOfCursorWord (query, cursor) {
+ if (cursor < 0 || cursor > query.length) {
+ // if outside range, just put cursor at the end
+ return query.length;
+ }
+ var right = query.slice(cursor);
+ return cursor + /^([^:\s]*:?).*?$/g.exec(right)[1].length
+ }
+
+ /*
+ * returns a function that will take a word and return suggestions from the given elements
+ * elements is a node list with data-key holding the keys to suggest.
+ */
+ function suggestFromElements(elements) {
+ return (function (word) {
+ var i, prefixed = [], contained = [], element, key, pos;
+ // keys is what I need to search in
+
+ // first, show keys that start with word
+ // second, show keys that match but don't start with the word
+
+ for (i = 0; i < elements.length; i++) {
+ element = elements[i];
+ key = element.dataset.key; // TODO fail gracefully? Noisily?
+ pos = key.indexOf(word);
+ if (pos === 0) {
+ prefixed.push(element);
+ } else if (pos > 0) {
+ contained.push(element);
+ }
+ }
+ return prefixed.concat(contained); // FIXME maybe need a good way to move non-matched elements
+ // without having to move them all back and forth repeatedly
+ });
+ }
+
+ }
+
+ return {
+ init: init
+ };
+})();
+
+
+// FIXME I can probably get all the behaviour working just from the wrapper, using element types
+
+// could offer to close quotes if quotes are open
+
+// could offer an 'exact match' option if a range is selected, which would place quotes around it (if it is not already quoted)
+// when no range selected, could have an exact match option that inserts "exact search text here" with the contents of the quotes in the selected range