mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-08-15 08:53:07 +09:00
292 lines
8.7 KiB
JavaScript
292 lines
8.7 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
(function() {
|
|
"use strict";
|
|
|
|
var DEBUG_ENABLED = false;
|
|
var MATCH_HIGHLIGHT_ACTIVE = "#f19750";
|
|
var MATCH_HIGHLIGHT_INACTIVE = "#ffde49";
|
|
var SCROLL_INTERVAL_INCREMENT = 5;
|
|
var SCROLL_INTERVAL_DURATION = 400;
|
|
var SCROLL_OFFSET = 60;
|
|
|
|
var activeHighlightSpan = null;
|
|
var lastSearch;
|
|
var scrollInterval;
|
|
var activeIndex = 0;
|
|
var highlightSpans = [];
|
|
|
|
function debug(str) {
|
|
if (DEBUG_ENABLED) {
|
|
console.log("FindInPage: " + str);
|
|
}
|
|
}
|
|
|
|
function isElementVisible(elem) {
|
|
return getComputedStyle(elem).visibility !== "hidden";
|
|
}
|
|
|
|
function isRectInViewport(rect) {
|
|
var left = rect.left + document.body.scrollLeft;
|
|
var right = rect.right + document.body.scrollLeft;
|
|
var top = rect.top + document.body.scrollTop;
|
|
var bottom = rect.bottom + document.body.scrollTop;
|
|
|
|
return rect.width > 0 &&
|
|
rect.height > 0 &&
|
|
right >= 0 &&
|
|
bottom >= 0 &&
|
|
left <= document.body.scrollWidth &&
|
|
top <= document.body.scrollHeight;
|
|
}
|
|
|
|
function findMatches(text) {
|
|
// For case-insensitive matching.
|
|
var lowerText = text.toLocaleLowerCase();
|
|
var upperText = text.toLocaleUpperCase();
|
|
|
|
var matches = [];
|
|
var range = document.createRange();
|
|
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
|
|
var textLength = text.length;
|
|
var node;
|
|
while (node = walker.nextNode()) {
|
|
var textContent = node.textContent;
|
|
findString: for (var i = 0; i < textContent.length - textLength + 1; ++i) {
|
|
for (var j = 0; j < textLength; ++j) {
|
|
var nextChar = textContent[i + j];
|
|
if (lowerText[j] !== nextChar && upperText[j] !== nextChar) {
|
|
continue findString;
|
|
}
|
|
}
|
|
|
|
// This node is a TextNode, not an Element. Its parent is the nearest Element.
|
|
var element = node.parentNode;
|
|
|
|
// Find the rect of just the text for this match.
|
|
range.setStart(node, i);
|
|
range.setEnd(node, i + textLength);
|
|
var textRect = range.getBoundingClientRect();
|
|
|
|
// We have a match, but we need to make sure it's visible. The condition
|
|
// below checks the following cases:
|
|
// * If this element or any of its parents has style visibility hidden.
|
|
// The visibility style is inherited, so we need to check only this
|
|
// element and not all of its ancestors.
|
|
// * If the highlight will be outside of the page's bounds. We determine
|
|
// this by comparing the bounds of the text rect.
|
|
// * If the element style display is set to none. display:none collapses
|
|
// the element's space, so this will again be detected by looking at
|
|
// the text's rect: if the element is collapsed, the width and height
|
|
// will be zero.
|
|
if (isElementVisible(element) && isRectInViewport(textRect)) {
|
|
matches.push({ node: node, index: i });
|
|
|
|
// Resume searching after this match to prevent overlapping results.
|
|
i += textLength- 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return matches;
|
|
}
|
|
|
|
function flattenNode(node) {
|
|
var parent = node.parentNode;
|
|
if (!parent) {
|
|
return;
|
|
}
|
|
|
|
while (node.firstChild) {
|
|
parent.insertBefore(node.firstChild, node);
|
|
}
|
|
|
|
node.remove();
|
|
parent.normalize();
|
|
}
|
|
|
|
function clearHighlights() {
|
|
if (highlightSpans.length > 0) {
|
|
for (var span of highlightSpans) {
|
|
flattenNode(span);
|
|
}
|
|
highlightSpans = [];
|
|
}
|
|
|
|
activeHighlightSpan = null;
|
|
}
|
|
|
|
function highlightAllMatches(text) {
|
|
debug("Searching: " + text);
|
|
|
|
clearHighlights();
|
|
|
|
if (!text.trim()) {
|
|
webkit.messageHandlers.findInPageHandler.postMessage({ totalResults: 0 });
|
|
return;
|
|
}
|
|
|
|
var range = document.createRange();
|
|
var matches = findMatches(text);
|
|
var highlightTemplate = document.createElement("span");
|
|
highlightTemplate.style.backgroundColor = MATCH_HIGHLIGHT_INACTIVE;
|
|
|
|
// If there are multiple matches in the same node, inserting a highlight span before other matches
|
|
// in that node will invalidate other matches since the node itself changes. By iterating through
|
|
// results in reverse, we highlight matches last in the node first so earlier matches are unaffected.
|
|
for (var i = matches.length - 1; i >= 0; --i) {
|
|
var match = matches[i];
|
|
var highlight = highlightTemplate.cloneNode();
|
|
|
|
range.setStart(match.node, match.index);
|
|
range.setEnd(match.node, match.index + text.length);
|
|
range.surroundContents(highlight);
|
|
highlightSpans.unshift(highlight);
|
|
}
|
|
|
|
debug(matches.length + " highlighted rects created!");
|
|
webkit.messageHandlers.findInPageHandler.postMessage({ totalResults: matches.length });
|
|
}
|
|
|
|
function getIDForRect(rect) {
|
|
return rect.top + "," + rect.bottom + "," + rect.left + "," + rect.right;
|
|
}
|
|
|
|
function updateActiveHighlight() {
|
|
// Reset the color of the previous highlight.
|
|
if (activeHighlightSpan) {
|
|
activeHighlightSpan.style.backgroundColor = MATCH_HIGHLIGHT_INACTIVE;
|
|
}
|
|
|
|
if (!highlightSpans.length) {
|
|
return;
|
|
}
|
|
|
|
activeHighlightSpan = highlightSpans[activeIndex];
|
|
activeHighlightSpan.style.backgroundColor = MATCH_HIGHLIGHT_ACTIVE;
|
|
|
|
// Find the position of the element centered on the screen, then scroll to it.
|
|
var rect = activeHighlightSpan.getBoundingClientRect();
|
|
var top = SCROLL_OFFSET + rect.top + scrollY - window.innerHeight / 2;
|
|
var left = rect.left + scrollX - window.innerWidth / 2;
|
|
left = clamp(left, 0, document.body.scrollWidth);
|
|
top = clamp(top, 0, document.body.scrollHeight);
|
|
scrollToSelection(left, top, SCROLL_INTERVAL_DURATION);
|
|
debug("Scrolled to: " + left + ", " + top);
|
|
}
|
|
|
|
function scrollToSelection(left, top, duration) {
|
|
var time = 0;
|
|
var startX = scrollX;
|
|
var startY = scrollY;
|
|
clearInterval(scrollInterval);
|
|
scrollInterval = setInterval(function() {
|
|
var xStep = easeOutCubic(time, startX, left - startX, duration);
|
|
var yStep = easeOutCubic(time, startY, top - startY, duration);
|
|
window.scrollTo(xStep, yStep);
|
|
time += SCROLL_INTERVAL_INCREMENT;
|
|
if (time >= duration) {
|
|
clearInterval(scrollInterval);
|
|
}
|
|
}, SCROLL_INTERVAL_INCREMENT);
|
|
}
|
|
|
|
function easeOutCubic(currentTime, startValue, changeInValue, duration) {
|
|
return changeInValue * (Math.pow(currentTime / duration - 1, 3) + 1) + startValue;
|
|
}
|
|
|
|
function clamp(number, min, max) {
|
|
return Math.max(min, Math.min(number, max));
|
|
}
|
|
|
|
function updateSearch(text) {
|
|
if (lastSearch == text) {
|
|
// The text is the same, so we're either finding either the next or previous result.
|
|
var totalResults = highlightSpans.length;
|
|
activeIndex = (activeIndex + totalResults) % totalResults;
|
|
} else {
|
|
// Store the current active rect to decide which new match should be active.
|
|
var activeHighlightRect = null;
|
|
if (activeHighlightSpan) {
|
|
activeHighlightRect = activeHighlightSpan.getBoundingClientRect();
|
|
}
|
|
|
|
// The search text changed, so scan the page for new results.
|
|
highlightAllMatches(text);
|
|
|
|
// If we found a match at or after the last match, use that position
|
|
// instead of starting again from the top.
|
|
activeIndex = 0;
|
|
if (activeHighlightRect) {
|
|
for (var i = 0; i < highlightSpans.length; i++) {
|
|
var highlight = highlightSpans[i];
|
|
var highlightRect = highlight.getBoundingClientRect();
|
|
if ((highlightRect.top == activeHighlightRect.top && highlightRect.left >= activeHighlightRect.left) ||
|
|
(highlightRect.top > activeHighlightRect.top)) {
|
|
activeIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
lastSearch = text;
|
|
}
|
|
|
|
// Update the UI with the current match index.
|
|
var currentResult = highlightSpans.length ? activeIndex + 1 : 0;
|
|
webkit.messageHandlers.findInPageHandler.postMessage({ currentResult: currentResult });
|
|
|
|
updateActiveHighlight();
|
|
}
|
|
|
|
if (!window.__firefox__) {
|
|
Object.defineProperty(window, '__firefox__', {
|
|
enumerable: false,
|
|
configurable: false,
|
|
writable: false,
|
|
value: {}
|
|
});
|
|
}
|
|
|
|
Object.defineProperty(window.__firefox__, 'find', {
|
|
enumerable: false,
|
|
configurable: false,
|
|
writable: false,
|
|
value: function(text) {
|
|
updateSearch(text);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(window.__firefox__, 'findNext', {
|
|
enumerable: false,
|
|
configurable: false,
|
|
writable: false,
|
|
value: function(text) {
|
|
activeIndex++;
|
|
updateSearch(text);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(window.__firefox__, 'findPrevious', {
|
|
enumerable: false,
|
|
configurable: false,
|
|
writable: false,
|
|
value: function(text) {
|
|
activeIndex--;
|
|
updateSearch(text);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(window.__firefox__, 'findDone', {
|
|
enumerable: false,
|
|
configurable: false,
|
|
writable: false,
|
|
value: function() {
|
|
clearHighlights();
|
|
lastSearch = null;
|
|
}
|
|
});
|
|
|
|
})();
|