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