| // 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}; |