Skip to content

Commit 03e87a5

Browse files
committed
implement basic devtools panel.onSearch interface and navigation between case in/sensitive finding
1 parent adab322 commit 03e87a5

File tree

10 files changed

+326
-151
lines changed

10 files changed

+326
-151
lines changed

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "console.diff(...)",
33
"description": "Compare in-memory objects and see the result inside devtools panel with a set of console.diff functions.",
4-
"version": "2.1",
4+
"version": "3.0.0",
55
"manifest_version": 3,
66
"minimum_chrome_version": "64.0",
77
"devtools_page": "src/jsdiff-devtools.html",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "jsdiff",
3-
"version": "2.1.0",
3+
"version": "3.0.0",
44
"description": "![jsdiff](./src/img/panel-icon64.png) --- Chrome devtools extension intended to display result of in-memory object comparisons with the help of dedicated commands invoked via console.",
55
"private": true,
66
"directories": {

src/js/bundle/jsdiff-panel.js

Lines changed: 147 additions & 63 deletions
Large diffs are not rendered by default.

src/js/jsdiff-devtools.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ function jsdiff_devtools_extension_api() {
138138
});
139139

140140
console.debug(
141-
'%c✚ console.diff',
141+
'%c✚ console.diff()',
142142
`
143143
font-weight: 700;
144144
color: #000;

src/js/jsdiff-proxy.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
(async () => {
1+
(() => {
22
window.addEventListener('message', (e) => {
33
if (
44
typeof e.data === 'object' &&

src/js/view/api/search.ts

Lines changed: 101 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,123 @@
11
type TSearchCommands = 'performSearch' | 'nextSearchResult' | 'cancelSearch';
2-
interface ISearchOptions {
3-
el: HTMLElement;
2+
3+
export interface ISearchOptions {
44
cmd: TSearchCommands;
55
query: string | null;
66
}
7-
const searchState = {
8-
lastCmd: '',
9-
currentEl: 0,
7+
8+
interface ISearchState {
9+
foundEls: HTMLElement[];
10+
query: string;
11+
currentIndex: number;
12+
clear: () => void;
13+
}
14+
15+
const searchState: ISearchState = {
16+
foundEls: [],
17+
query: '',
18+
currentIndex: -1,
19+
clear() {
20+
this.foundEls.splice(0);
21+
this.query = '';
22+
this.currentIndex = -1;
23+
},
1024
};
1125

12-
function highlightElements(container: HTMLElement, query: string): void {
13-
const childNodes = container.childNodes;
26+
const CLASS_FOUND = 'jsdiff-found';
27+
const CLASS_FOUND_THIS = 'jsdiff-found-this';
28+
const uppercasePattern = /\p{Lu}/u; // 'u' flag enables Unicode matching
29+
30+
function hasUppercaseCharacter(str: string): boolean {
31+
for (let n = 0, N = str.length; n < N; n++) {
32+
if (uppercasePattern.test(str.charAt(n))) {
33+
return true;
34+
}
35+
}
36+
37+
return false;
38+
}
39+
40+
function highlightElements(
41+
container: HTMLElement,
42+
query: string,
43+
foundEls: HTMLElement[]
44+
): void {
45+
const containerNodes = container.childNodes as NodeListOf<HTMLElement>;
46+
47+
if (containerNodes.length) {
48+
const firstChild = containerNodes[0];
49+
50+
if (containerNodes.length === 1 && firstChild.nodeType === Node.TEXT_NODE) {
51+
const text = firstChild.textContent || firstChild.innerText;
52+
const found = hasUppercaseCharacter(query)
53+
? text.includes(query) // case-sensitive
54+
: text.toLocaleLowerCase().includes(query); // case-insensitive
1455

15-
for (let n = 0, N = childNodes.length; n < N; n++) {
16-
const child = childNodes[n];
17-
if (child.nodeType === Node.ELEMENT_NODE) {
18-
const element = child as HTMLElement;
19-
const text = element.textContent || element.innerText;
20-
if (text.includes(query)) {
21-
element.classList.add('found');
56+
if (found) {
57+
container.classList.add('jsdiff-found');
58+
foundEls.push(container);
59+
}
60+
} else {
61+
for (let n = 0, N = containerNodes.length; n < N; n++) {
62+
const child = containerNodes[n];
63+
64+
if (child.nodeType === Node.ELEMENT_NODE) {
65+
highlightElements(child, query, foundEls); // recursion
66+
}
2267
}
23-
highlightElements(element, query); // recursion
2468
}
2569
}
2670
}
2771

28-
function clearHighlight(container: HTMLElement) {
29-
const allFound = container.querySelectorAll('.found');
72+
function clearHighlight(container: HTMLElement): void {
73+
const allFound = container.querySelectorAll(`.${CLASS_FOUND}`);
3074

31-
for (let n = 0, N = allFound.length; n < N; n++) {
32-
allFound[n].classList.remove('found', 'foundThis');
75+
for (let n = allFound.length - 1; n >= 0; n--) {
76+
allFound[n].classList.remove(CLASS_FOUND, CLASS_FOUND_THIS);
3377
}
3478
}
3579

36-
export function searchQueryInDom({ el, cmd, query }: ISearchOptions) {
80+
function highlightCurrentResult(searchState: ISearchState): void {
81+
searchState.currentIndex++;
82+
searchState.currentIndex %= searchState.foundEls.length;
83+
84+
const el = searchState.foundEls[searchState.currentIndex];
85+
el.classList.add(CLASS_FOUND_THIS);
86+
el.scrollIntoView({
87+
behavior: 'smooth',
88+
block: 'nearest',
89+
inline: 'nearest',
90+
});
91+
}
92+
93+
export function searchQueryInDom(
94+
el: HTMLElement,
95+
{ cmd, query }: ISearchOptions
96+
): void {
3797
query = (typeof query === 'string' && query.trim()) || '';
38-
console.log('🔦', cmd, query);
3998

40-
if ('performSearch' === cmd && query) {
41-
highlightElements(el, query);
42-
} else if ('nextSearchResult' === cmd) {
43-
// TODO: ...
99+
// console.log('🔦', cmd, query);
100+
101+
if ('performSearch' === cmd) {
102+
searchState.query = query;
103+
104+
if (!query) {
105+
searchState.clear();
106+
clearHighlight(el);
107+
}
108+
} else if ('nextSearchResult' === cmd && searchState.query) {
109+
clearHighlight(el);
110+
searchState.foundEls.splice(0);
111+
highlightElements(el, searchState.query, searchState.foundEls);
112+
113+
if (searchState.foundEls.length) {
114+
highlightCurrentResult(searchState);
115+
} else {
116+
searchState.clear();
117+
clearHighlight(el);
118+
}
44119
} else if ('cancelSearch' === cmd) {
120+
searchState.clear();
45121
clearHighlight(el);
46122
}
47123
}

src/js/view/api/toolkit.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function hasValue(o: unknown): boolean {
2+
return undefined !== o && null !== o;
3+
}

src/js/view/app.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ import { createApp } from 'vue';
22
import Panel from './panel.vue';
33

44
const app = createApp(Panel);
5-
app.mount('#app');
5+
app.mount('#jsdiff-app');

0 commit comments

Comments
 (0)