This repository has been archived by the owner on Nov 9, 2017. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
23739ee
commit a0adf88
Showing
2 changed files
with
241 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
239 changes: 239 additions & 0 deletions
239
zanata-war/src/main/resources/org/zanata/webtrans/public/search-field-suggestions.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<suggested.length; i++) { | ||
resultList.appendChild(suggested[i]); | ||
} | ||
} | ||
|
||
function hideSuggestions () { | ||
resultList.classList.add('is-invisible'); | ||
} | ||
|
||
function showSuggestions () { | ||
resultList.classList.remove('is-invisible'); | ||
} | ||
|
||
// TODO need to trigger on more than just keyup (when focused and on paste as well I think, and when clicked or cursor moves) | ||
input.addEventListener('keyup', calculateSuggestions); | ||
input.addEventListener('focus', calculateSuggestions); | ||
input.addEventListener('blur', function () { | ||
// clicking a suggestion will cause blur, but hiding suggestions | ||
// interferes with the click event on the suggestion. | ||
// if a suggestion was clicked, focus will return to the input. | ||
setTimeout(function () { | ||
if (document.activeElement !== input) { | ||
hideSuggestions(); | ||
} | ||
}, 100); | ||
}); | ||
input.addEventListener('click', function () { | ||
clearResultSelection(); | ||
calculateSuggestions(); | ||
}); | ||
|
||
// FIXME this function looks slightly daunting, make it more elegant | ||
input.addEventListener('keydown', function (e) { | ||
var key = e.keyCode, | ||
selected = resultList.querySelector('.is-selected'), | ||
nextElement; | ||
if (key === 27) { // Esc | ||
selected.classList.remove('is-selected'); | ||
} else if ((key === 13 || key === 39) && selected) { // Enter or Right-arrow | ||
e.preventDefault(); | ||
insertTargetResult(selected); | ||
} else if (key === 13) { // Enter with no selection, trigger search | ||
valueChangeCallback(input.value); | ||
} else if (key === 38 && selected) { // Up arrow | ||
e.preventDefault(); | ||
nextElement = selected.previousElementSibling; | ||
if (nextElement) { | ||
selectTargetResult(nextElement); | ||
} else { | ||
clearResultSelection(); | ||
} | ||
} else if (key === 40) { // Down arrow | ||
e.preventDefault(); | ||
if (selected) { | ||
nextElement = selected.nextElementSibling; | ||
if (nextElement) { | ||
selectTargetResult(nextElement); | ||
} | ||
} else { | ||
nextElement = resultList.querySelector('.js-suggest__result'); | ||
if (nextElement) { | ||
nextElement.classList.add('is-selected'); | ||
} | ||
} | ||
} | ||
}); | ||
|
||
|
||
/* | ||
* Returns the word that the cursor is touching. Empty string if the cursor is not touching a word. null if the cursor is in a quoted section. | ||
* FIXME returning '' or null is a bit crude, find a better way. | ||
* | ||
* cursor: int position within query of cursor | ||
*/ | ||
function getCursorWord (query, cursor) { | ||
if (cursor < 0 || cursor > 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 |