blob: 0b4eda073260f8628842ea044d45a1faaf059c8a [file] [log] [blame]
// Copyright (C) 2019 Apple Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
import {DOM, REF} from '/library/js/Ref.js';
import {QueryModifier, paramsToQuery} from '/assets/js/common.js';
const LIMIT = 10;
function SearchBar(callback, suites) {
let mouseCount = 0;
let closeSearch = () => {};
let inFlight = 0;
const candidatesRef = REF.createRef({
state: {candidates: {}, selected: null, displayed: false},
onStateUpdate: (element, stateDiff, state) => {
const selected = stateDiff.selected ? stateDiff.selected : state ? state.selected : null;
const candidates = stateDiff.candidates ? stateDiff.candidates : state ? state.candidates : [];
if (stateDiff.candidates) {
let count = 0;
DOM.inject(element, Object.keys(candidates).map(key => {
return candidates[key].map(test => {
const candidateRef = REF.createRef({
onElementMount: element => {
element.onmouseover = () => {
++mouseCount;
candidatesRef.setState({selected: {suite: key, test: test}});
};
element.onmouseout = () => {mouseCount = mouseCount > 0 ? mouseCount - 1 : 0};
element.onclick = () => {
callback({suite: key, test: test});
closeSearch();
}
}
});
++count;
return `<li ref="${candidateRef}" style="cursor: pointer;">${test} (${key})</li>`;
}).join('');
}).join('') + `<div class="spinner tiny" ${inFlight > 0 ? '' : 'style="display: none;"'}></div>`);
if (count)
element.style.display = 'block';
else {
element.style.display = 'none';
mouseCount = 0;
stateDiff.selected = null;
}
}
let index = 0;
Object.keys(candidates).forEach(key => {
candidates[key].forEach(test => {
if (selected && key === selected.suite && test === selected.test)
element.children[index].classList.add('selected');
else
element.children[index].classList.remove('selected');
++index;
});
});
element.children[index].style.display = inFlight > 0 ? 'block' : 'none';
}
});
let candidates = {};
let currentDispatch = Date.now();
const nextSelected = (moveUp = false) => {
const forIndexIn = moveUp ? (list, callback) => {
for (let index = list.length - 1; index >= 0; --index) {
if (!callback(index))
return false;
}
return true;
} : (list, callback) => {
for (let index = 0; index < list.length; ++index) {
if (!callback(index))
return false;
}
return true;
}
let didFind = false;
let boundry = null;
const suites = Object.keys(candidates);
forIndexIn(suites, suiteIndex => {
const tests = candidates[suites[suiteIndex]];
return forIndexIn(tests, testIndex => {
if (!boundry)
boundry = {
suite: suites[suiteIndex],
test: tests[testIndex],
};
if (!candidatesRef.state.selected)
return false;
if (didFind) {
candidatesRef.setState({
selected: {
suite: suites[suiteIndex],
test: tests[testIndex],
},
});
return false;
}
if (candidatesRef.state.selected.suite == suites[suiteIndex] && candidatesRef.state.selected.test == tests[testIndex])
didFind = true;
return true;
});
});
if (!didFind)
candidatesRef.setState({selected: boundry});
};
const inputRef = REF.createRef({
onElementMount: element => {
closeSearch = () => {
currentDispatch = Date.now();
element.value = '';
candidates = {};
candidatesRef.setState({candidates: {}, selected: null});
mouseCount = 0;
element.blur();
}
element.addEventListener('focus', () => {candidatesRef.setState({candidates: candidates});});
element.addEventListener('blur', () => {
if (!mouseCount)
candidatesRef.setState({candidates: {}});
});
element.addEventListener('keyup', event => {
if (event.keyCode == 0x28 /* DOM_VK_DOWN */ || event.keyCode == 0x26 /* DOM_VK_UP */)
return;
const inputValue = element.value.trimStart().trimEnd();
if (!inputValue)
return;
let myDispatch = Date.now();
candidates = {};
suites.forEach(suite => {
inFlight += 1;
fetch(`api/${suite}/tests?limit=${LIMIT}&test=${inputValue}`).then(response => {
inFlight -= 1;
if (myDispatch < currentDispatch) {
candidatesRef.setState({});
return;
}
currentDispatch = Math.max(currentDispatch, myDispatch);
response.json().then(json => {
candidates[suite] = json;
if (json.length)
candidatesRef.setState({candidates: candidates});
else
candidatesRef.setState({});
});
}).catch(error => {
// If the load fails, log the error and continue
console.error(JSON.stringify(error, null, 4));
candidatesRef.setState({});
});
});
});
element.addEventListener('keydown', event => {
if (event.keyCode == 0x28 /* DOM_VK_DOWN */) {
nextSelected(false);
} else if (event.keyCode == 0x26 /* DOM_VK_UP */) {
nextSelected(true);
} else if (event.keyCode == 0x0D /* VK_RETURN */) {
if (candidatesRef.state.selected)
callback(candidatesRef.state.selected);
else {
let pairs = [];
Object.keys(candidates).forEach(suite => {
candidates[suite].forEach(test => {
pairs.push({
suite: suite,
test: test,
});
});
});
if (!pairs.length)
return;
callback(...pairs);
}
closeSearch();
} else if (event.keyCode == 0x1B /* DOM_VK_ESCAPE */)
candidatesRef.setState({candidates: {}});
});
element.onpaste = (event) => {
let ignoring = false;
let tests = new Set();
event.clipboardData.getData('text').split(/\s+/g).forEach(str => {
if (str === '[')
ignoring = true;
if (ignoring) {
if (str === ']')
ignoring = false;
return;
}
if (str)
tests.add(str);
});
tests = [...tests];
if (tests.length <= 1)
return;
let pairs = [];
let outstanding = suites.length;
const query = paramsToQuery({test: tests});
suites.forEach(suite => {
fetch(`api/${suite}/tests?${query}`).then(response => {
response.json().then(json => {
json.forEach(test => {
pairs.push({
suite: suite,
test: test,
});
});
outstanding -= 1;
if (outstanding <= 0) {
callback(...pairs);
closeSearch();
}
});
}).catch(error => {
// If the load fails, log the error and continue
console.error(JSON.stringify(error, null, 4));
candidatesRef.setState({});
});
});
}
}
});
return `<div class="input">
<input type="text" ref="${inputRef}" autocomplete="off" autocapitalize="none" required/>
<label>Search test</label>
</div>
<ul class="search-candidates" ref="${candidatesRef}" style="display: none;"></ul>`;
}
export {SearchBar};