blob: fdaa237e62c40f276cced1594e9bd31729b6028d [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
cr.define('cr.search_highlight_utils', function() {
/** @type {string} */
const WRAPPER_CSS_CLASS = 'search-highlight-wrapper';
/** @type {string} */
const ORIGINAL_CONTENT_CSS_CLASS = 'search-highlight-original-content';
/** @type {string} */
const HIT_CSS_CLASS = 'search-highlight-hit';
/** @type {string} */
const SEARCH_BUBBLE_CSS_CLASS = 'search-bubble';
/**
* Replaces the the highlight wrappers given in |wrappers| with the original
* search nodes.
* @param {!Array<!Node>} wrappers
*/
function removeHighlights(wrappers) {
for (const wrapper of wrappers) {
// If wrapper is already removed, do nothing.
if (!wrapper.parentElement) {
continue;
}
const textNode =
wrapper.querySelector(`.${ORIGINAL_CONTENT_CSS_CLASS}`).firstChild;
wrapper.parentElement.replaceChild(textNode, wrapper);
}
}
/**
* Finds all previous highlighted nodes under |node| and replaces the
* highlights (yellow rectangles) with the original search node. Searches only
* within the same shadowRoot and assumes that only one highlight wrapper
* exists under |node|.
* @param {!Node} node
*/
function findAndRemoveHighlights(node) {
const wrappers = Array.from(node.querySelectorAll(`.${WRAPPER_CSS_CLASS}`));
assert(wrappers.length == 1);
removeHighlights(wrappers);
}
/**
* Applies the highlight UI (yellow rectangle) around all matches in |node|.
* @param {!Node} node The text node to be highlighted. |node| ends up
* being hidden.
* @param {!Array<string>} tokens The string tokens after splitting on the
* relevant regExp. Even indices hold text that doesn't need highlighting,
* odd indices hold the text to be highlighted. For example:
* const r = new RegExp('(foo)', 'i');
* 'barfoobar foo bar'.split(r) => ['bar', 'foo', 'bar ', 'foo', ' bar']
* @return {!Node} The new highlight wrapper.
*/
function highlight(node, tokens) {
const wrapper = document.createElement('span');
wrapper.classList.add(WRAPPER_CSS_CLASS);
// Use existing node as placeholder to determine where to insert the
// replacement content.
node.parentNode.replaceChild(wrapper, node);
// Keep the existing node around for when the highlights are removed. The
// existing text node might be involved in data-binding and therefore should
// not be discarded.
const span = document.createElement('span');
span.classList.add(ORIGINAL_CONTENT_CSS_CLASS);
span.style.display = 'none';
span.appendChild(node);
wrapper.appendChild(span);
for (let i = 0; i < tokens.length; ++i) {
if (i % 2 == 0) {
wrapper.appendChild(document.createTextNode(tokens[i]));
} else {
const hitSpan = document.createElement('span');
hitSpan.classList.add(HIT_CSS_CLASS);
hitSpan.style.backgroundColor = '#ffeb3b'; // var(--paper-yellow-500)
hitSpan.style.color = '#202124'; // var(--google-grey-900)
hitSpan.textContent = tokens[i];
wrapper.appendChild(hitSpan);
}
}
return wrapper;
}
/**
* Highlights an HTML element by displaying a search bubble. The element
* should already be visible or the bubble will render incorrectly.
* @param {!HTMLElement} element The element to be highlighted.
* @param {string} rawQuery The search query.
* @return {?Node} The search bubble that was added, or null if no new bubble
* was added.
* @private
*/
function highlightControlWithBubble(element, rawQuery) {
let searchBubble = element.querySelector(`.${SEARCH_BUBBLE_CSS_CLASS}`);
// If the element has already been highlighted, there is no need to do
// anything.
if (searchBubble) {
return null;
}
searchBubble = document.createElement('div');
searchBubble.classList.add(SEARCH_BUBBLE_CSS_CLASS);
const innards = document.createElement('div');
innards.classList.add('search-bubble-innards');
innards.textContent = rawQuery;
searchBubble.appendChild(innards);
element.appendChild(searchBubble);
const updatePosition = function() {
searchBubble.style.top = element.offsetTop +
(innards.classList.contains('above') ? -searchBubble.offsetHeight :
element.offsetHeight) +
'px';
};
updatePosition();
searchBubble.addEventListener('mouseover', function() {
innards.classList.toggle('above');
updatePosition();
});
return searchBubble;
}
return {
removeHighlights: removeHighlights,
findAndRemoveHighlights: findAndRemoveHighlights,
highlight: highlight,
highlightControlWithBubble: highlightControlWithBubble,
};
});