Skip to content

Commit 8c5bbb2

Browse files
authoredJun 26, 2023
v3.0.0 (#11)
+ migrate to chrome extension manifest v3 + update build stack + support internal search + minor improvements
1 parent 65e5822 commit 8c5bbb2

25 files changed

+2580
-522
lines changed
 

‎.gitignore

+4-1
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,7 @@ typings/
6161
.DS_Store
6262

6363
*.zip
64-
package-lock.json
64+
*.js.map
65+
66+
# typescript output
67+
.ts-built

‎.prettierignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/src/js/bundle
2+
.ts-built
3+
*.min.js

‎.prettierrc.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"singleQuote": true
3+
}

‎README.md

+77-27
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,96 @@
11
### ![](./src/img/panel-icon28.png) console.diff()
2+
23
[![console.diff()](https://storage.googleapis.com/web-dev-uploads/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/tbyBjqi7Zu733AAKA5n4.png)](https://chrome.google.com/webstore/detail/jsdiff-devtool/iefeamoljhdcpigpnpggeiiabpnpgonb)
34

45
Chrome devtools extension intended to display result of deep in-memory object
56
comparisons with the help of dedicated console commands.
67

8+
<details>
9+
<summary> <strong>Screenshots</strong> </summary>
10+
11+
- Comparing two objects
12+
![screenshot](./src/img/screenshot-01.png)
13+
14+
- Tracking changes in localStorage (unchanged are hidden)
15+
![screenshot](./src/img/screenshot-02.png)
16+
17+
</details>
18+
19+
### Based on
20+
21+
- [jsondiffpatch](https://github.com/benjamine/jsondiffpatch) by Benjamín Eidelman
22+
- [vuejs](https://github.com/vuejs) by Evan You
23+
724
### Features
8-
* compare objects from multiple tabs and/or between page reloads
9-
* function code included in comparison result in form of a string, may help to see if it was altered
10-
* document, dom-elements and other non-serializable objects are filtered-out from the results
11-
* self recurring references displayed only once, the rest of occurrences are filtered-out
25+
26+
- compare objects from multiple tabs and/or between page reloads
27+
- function code included in comparison result in form of a string, may help to see if it was altered
28+
- document, dom-elements and other non-serializable objects are filtered-out from the results
29+
- self recurring references displayed only once, the rest of occurrences are filtered-out
30+
- basic integration with search functionality within devtools
31+
- if search query contains upper-case letter - the search will be case-sensitive
32+
33+
### Limitations and workarounds
34+
35+
- some instances of objects may cause exception during preparations for comparison
36+
- try to narrow compared contexts
37+
- if it's some Browser API that causes an exception and not a framework, consider opening an issue,
38+
so it will be possible to solve it on a permanent basis
39+
- while paused in debug mode, JSDiff panel won't reflect the result until runtime is resumed ([#10][i10])
40+
41+
[i10]: https://github.com/zendive/jsdiff/issues/10
1242

1343
### API
44+
45+
- **console.diff(left, right)** - compare left and right arguments
46+
1447
```javascript
15-
console.diff(left, right); // compare left and right
16-
console.diff(next); // shorthand of diffPush while single argumented
17-
console.diffLeft(left); // update object on the left side only
18-
console.diffRight(right); // update object on the right side only
19-
console.diffPush(next); // shifts sides, right becomes left, next becomes right
48+
console.diff({ a: 1, b: 1, c: 3 }, { a: 1, b: 2, d: 3 });
49+
```
50+
51+
- **console.diffPush(next)** - shifts sides, right becomes left, next becomes right
52+
53+
```javascript
54+
console.diffPush(Date.now());
55+
```
56+
57+
- **console.diff(next)** - shorthand for `diffPush`
58+
59+
```javascript
60+
console.diff(Date.now());
61+
```
62+
63+
- **console.diffLeft(left)** - update the old value only
64+
65+
```javascript
66+
console.diffLeft(Date.now());
67+
```
68+
69+
- **console.diffRight(right)** - update the new value only
70+
71+
```javascript
72+
console.diffRight(Date.now());
2073
```
2174

2275
### Usage basics
76+
2377
Historically, left side represents the old state and right side the new state.
24-
* Things that are present on the left side but missing on the right side are colour-coded as red (old).
25-
* Things that are missing on the left side but present on the right side are colour-coded as green (new).
78+
79+
- Things that are present on the left side but missing on the right side are colour-coded as red (old).
80+
- Things that are missing on the left side but present on the right side are colour-coded as green (new).
2681

2782
To track changes of the same variable in timed manner you can push it with `diffPush` or `diff`
28-
with a single argument,
29-
that will shift objects from right to left, showing differences with previous push state.
83+
with a single argument, that will shift objects from right to left, showing differences with previous push state.
3084

3185
### How it works
32-
* `jsdiff-devtools` registers devtools panel
33-
* injects console commands that send data to `jsdiff-proxy`
34-
* injects `jsdiff-proxy` to farther communicate objects to the extension's `jsdiff-background`
35-
* when `console.diff` command invoked
36-
* argument/s are cloned in a suitable form for sending between different window contexts and sent to `jsdiff-proxy`
37-
* `jsdiff-proxy` catches the data and sends it to the `jsdiff-background` where it is stored for future consuming
38-
* when `jsdiff-panel` is mounted (visible in devtools) it listens to data expected to come from the `jsdiff-background`
39-
and displays it
40-
41-
### Screenshot
42-
![screenshot](./src/img/screenshot-01.png)
4386

44-
### Based on
45-
- [jsondiffpatch](https://github.com/benjamine/jsondiffpatch) by Benjamín Eidelman
46-
- [vuejs](https://github.com/vuejs) by Evan You
87+
- `jsdiff-devtools.js` registers devtools panel
88+
- injects `console.diff` commands into inspected window's console interface
89+
- each function clones arguments and sends them via `postMessage` to `jsdiff-proxy.js` in `jsdiff-console-to-proxy` message
90+
- injects `jsdiff-proxy.js` that listens on window `jsdiff-console-to-proxy` message and sends it further to chrome runtime in `jsdiff-proxy-to-devtools` message
91+
- listens on `jsdiff-proxy-to-devtools` and prepares payload for `vue/panel.js` and sends it with `jsdiff-devtools-to-panel-compare` message
92+
- when user invokes devtools search command - informs `vue/panel.js` with `jsdiff-devtools-to-panel-search` message
93+
- when `vue/panel.js` is visible in devtools
94+
- reflects result of last compare request
95+
- listens on `jsdiff-devtools-to-panel-compare` requests
96+
- listens on `jsdiff-devtools-to-panel-search` and tries to find query in DOM

‎manifest.json

+6-17
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,15 @@
11
{
22
"name": "console.diff(...)",
3-
"description": "Deep compare complex in-memory objects inside browser devtools panel with console.diff command.",
4-
"version": "2.0",
5-
"manifest_version": 2,
6-
"minimum_chrome_version": "64.0",
3+
"description": "Compare in-memory objects and see the result inside devtools panel with a set of console.diff functions.",
4+
"version": "3.0.0",
5+
"manifest_version": 3,
6+
"minimum_chrome_version": "66.0",
77
"devtools_page": "src/jsdiff-devtools.html",
88
"icons": {
99
"28": "src/img/panel-icon28.png",
1010
"64": "src/img/panel-icon64.png",
1111
"128": "src/img/panel-icon128.png"
1212
},
13-
"background": {
14-
"persistent": false,
15-
"scripts": [
16-
"src/js/jsdiff-background.js"
17-
]
18-
},
19-
"content_security_policy": "script-src 'self'; object-src 'self'",
20-
"permissions": [
21-
"http://*/*",
22-
"https://*/*",
23-
"file:///*",
24-
"clipboardWrite"
25-
]
13+
"permissions": ["storage", "scripting", "activeTab", "clipboardWrite"],
14+
"host_permissions": ["*://*/*"]
2615
}

‎package.json

+20-17
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
{
22
"name": "jsdiff",
3-
"version": "2.0.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": {
77
"doc": "doc"
88
},
99
"scripts": {
1010
"test": "echo \"no test\" && exit 1",
11-
"dev": "webpack --progress --watch",
12-
"prod": "webpack --progress -p"
11+
"dev": "webpack --progress --watch --mode=development",
12+
"prod": "NODE_ENV=production webpack --mode=production",
13+
"format": "prettier . --write"
1314
},
1415
"repository": {
1516
"type": "git",
@@ -26,21 +27,23 @@
2627
"url": "https://github.com/zendive/jsdiff/issues"
2728
},
2829
"homepage": "https://github.com/zendive/jsdiff#readme",
30+
"type": "module",
2931
"devDependencies": {
30-
"clean-webpack-plugin": "~1.0.1",
31-
"css-loader": "~2.1.0",
32-
"file-loader": "^3.0.1",
33-
"node-sass": "~4.14.1",
34-
"sass-loader": "~7.1.0",
35-
"style-loader": "~0.23.1",
36-
"vue-loader": "~15.6.2",
37-
"vue-template-compiler": "~2.6.6",
38-
"webpack": "~4.29.0",
39-
"webpack-cli": "~3.2.1"
40-
},
41-
"dependencies": {
32+
"@types/chrome": "^0.0.237",
33+
"@vue/compiler-sfc": "^3.3.4",
34+
"clean-webpack-plugin": "~4.0.0",
35+
"css-loader": "~6.8.1",
4236
"jsondiffpatch": "~0.4.1",
43-
"moment": "~2.29.0",
44-
"vue": "~2.6.12"
37+
"prettier": "^2.8.8",
38+
"sass": "^1.63.3",
39+
"sass-loader": "~13.3.2",
40+
"style-loader": "~3.3.3",
41+
"ts-loader": "^9.4.3",
42+
"typescript": "^5.1.3",
43+
"vue": "~3.3.4",
44+
"vue-loader": "~17.2.2",
45+
"webpack": "~5.86.0",
46+
"webpack-bundle-analyzer": "^4.9.0",
47+
"webpack-cli": "~5.1.4"
4548
}
4649
}

‎pnpm-lock.yaml

+1,711
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/img/screenshot-00.png

20.8 KB
Loading

‎src/img/screenshot-01.png

24 KB
Loading

‎src/img/screenshot-02.png

48.3 KB
Loading

‎src/js/bundle/jsdiff-panel.js

+1-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/js/jsdiff-background.js

-66
This file was deleted.

‎src/js/jsdiff-devtools.js

+124-82
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,109 @@
1-
// resistance is futile
2-
3-
// Create panel
4-
chrome.devtools.panels.create(
1+
// tabId may be null if user opened the devtools of the devtools
2+
if (chrome.devtools.inspectedWindow.tabId !== null) {
3+
chrome.devtools.panels.create(
54
'JSDiff',
65
'/src/img/panel-icon28.png',
76
'/src/jsdiff-panel.html',
87
(panel) => {
9-
//panel.onSearch.addListener(sendMessage('jsdiff-panel-search'));
10-
//panel.onShown.addListener(sendMessage('jsdiff-panel-shown'));
11-
//panel.onHidden.addListener(sendMessage('jsdiff-panel-hidden'));
8+
panel.onSearch.addListener(async (cmd, query) => {
9+
await chrome.runtime.sendMessage({
10+
source: 'jsdiff-devtools-to-panel-search',
11+
params: { cmd, query },
12+
});
13+
});
1214
}
13-
);
15+
);
1416

15-
// Create a connection to the background page
16-
if (chrome.devtools.inspectedWindow.tabId !== null) {
17-
// tabId may be null if user opened the devtools of the devtools
18-
const backgroundPageConnection = chrome.runtime.connect({name: 'jsdiff-devtools'});
19-
backgroundPageConnection.postMessage({
20-
name: 'init',
21-
tabId: chrome.devtools.inspectedWindow.tabId
17+
injectScripts();
18+
19+
// listen on tabs page reload
20+
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
21+
if ('complete' === changeInfo.status) {
22+
injectScripts();
23+
}
2224
});
2325

24-
injectScripts();
25-
}
26+
// track api invocation - (api can be invoked prior opening of jsdiff panel)
27+
chrome.runtime.onMessage.addListener(async (req) => {
28+
if ('jsdiff-proxy-to-devtools' === req.source) {
29+
const payload = req.payload;
30+
let { lastApiReq } = await chrome.storage.local.get(['lastApiReq']);
31+
if (!lastApiReq) {
32+
lastApiReq = {
33+
left: '(empty)',
34+
right: '(empty)',
35+
timestamp: Date.now(),
36+
};
37+
}
2638

27-
// listen on tabs page reload
28-
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
29-
if ('complete' === changeInfo.status) {
30-
injectScripts();
31-
}
32-
});
39+
if (Object.getOwnPropertyDescriptor(payload, 'push')) {
40+
lastApiReq.left = lastApiReq.right;
41+
lastApiReq.right = payload.push;
42+
} else {
43+
if (Object.getOwnPropertyDescriptor(payload, 'left')) {
44+
lastApiReq.left = payload.left;
45+
}
46+
if (Object.getOwnPropertyDescriptor(payload, 'right')) {
47+
lastApiReq.right = payload.right;
48+
}
49+
}
50+
lastApiReq.timestamp = payload.timestamp;
51+
52+
chrome.storage.local.set({ lastApiReq: lastApiReq });
53+
chrome.runtime.sendMessage({
54+
source: 'jsdiff-devtools-to-panel-compare',
55+
payload: lastApiReq,
56+
});
57+
}
58+
});
59+
}
3360

3461
// Inject console api and messaging proxy
3562
// us shown at: https://developer.chrome.com/extensions/devtools#content-script-to-devtools
3663
function injectScripts() {
3764
chrome.devtools.inspectedWindow.eval(
38-
`;(${jsdiff_devtools_extension_api.toString()})();`, {
39-
useContentScriptContext: false // default: false
40-
}, (res, error) => {
41-
if (res && !error) {
42-
// injected
43-
chrome.tabs.executeScript(chrome.devtools.inspectedWindow.tabId, {
44-
file: '/src/js/jsdiff-proxy.js'
45-
});
46-
}
47-
});
65+
`;(${jsdiff_devtools_extension_api.toString()})();`,
66+
{
67+
useContentScriptContext: false, // default: false
68+
},
69+
(res, error) => {
70+
if (res && !error) {
71+
const tabId = chrome.devtools.inspectedWindow.tabId;
72+
73+
chrome.scripting
74+
.executeScript({
75+
target: {
76+
tabId,
77+
allFrames: true,
78+
},
79+
files: ['/src/js/jsdiff-proxy.js'],
80+
})
81+
.then(
82+
() => {
83+
// console.log('script injected in all frames');
84+
},
85+
(error) => {
86+
// console.error('script inject failed', error);
87+
}
88+
);
89+
}
90+
}
91+
);
4892
}
4993

5094
function jsdiff_devtools_extension_api() {
51-
if (typeof(console.diff) === 'function') {
52-
/*already injected*/
95+
if (typeof console.diff === 'function') {
96+
/* already injected */
5397
return false;
5498
}
5599

56100
function postDataAdapter(set, key, value) {
57101
try {
58-
if (
59-
value instanceof Element ||
60-
value instanceof Document
61-
) {
102+
if (value instanceof Element || value instanceof Document) {
62103
return undefined;
63-
} else if (typeof(value) === 'function') {
104+
} else if (typeof value === 'function') {
64105
return value.toString();
65-
} else if (
66-
value instanceof Object ||
67-
typeof(value) === 'object'
68-
) {
106+
} else if (value instanceof Object || typeof value === 'object') {
69107
if (set.has(value)) {
70108
return undefined;
71109
}
@@ -82,65 +120,69 @@ function jsdiff_devtools_extension_api() {
82120
try {
83121
['push', 'left', 'right'].forEach((key) => {
84122
if (payload.hasOwnProperty(key)) {
85-
let set = new Set();
86-
payload[key] = JSON.parse(
87-
JSON.stringify(
88-
payload[key],
89-
postDataAdapter.bind(null, set)
90-
)
91-
);
92-
set.clear();
93-
set = null;
123+
const value = payload[key];
124+
125+
if (value === undefined) {
126+
payload[key] = '(undefined)';
127+
} else if (value === null) {
128+
payload[key] = '(null)';
129+
} else {
130+
let set = new Set();
131+
payload[key] = JSON.parse(
132+
JSON.stringify(value, postDataAdapter.bind(null, set))
133+
);
134+
set.clear();
135+
set = null;
136+
}
94137
}
95138
});
96-
window.postMessage({payload, source: 'jsdiff-devtools-extension-api'}, '*');
139+
140+
window.postMessage(
141+
{
142+
payload,
143+
source: 'jsdiff-console-to-proxy',
144+
},
145+
'*'
146+
);
97147
} catch (e) {
98-
console.error('%cJSDiff', `
148+
console.error(
149+
'%cconsole.diff',
150+
`
99151
font-weight: 700;
100152
color: #000;
101-
background-color: yellow;
153+
background-color: #ffbbbb;
102154
padding: 2px 4px;
103155
border: 1px solid #bbb;
104156
border-radius: 4px;
105-
`, e);
157+
`,
158+
e
159+
);
106160
}
107161
}
108162

109163
Object.assign(console, {
110-
diff(left, right) {
111-
post(right === undefined? {push: left} : {left, right});
112-
},
113-
114-
diffLeft(left) {
115-
post({left});
116-
},
117-
118-
diffRight(right) {
119-
post({right});
120-
},
121-
122-
diffPush(push) {
123-
post({push});
124-
},
164+
diff: (...args) =>
165+
post(
166+
args.length === 1
167+
? { push: args[0], timestamp: Date.now() }
168+
: { left: args[0], right: args[1], timestamp: Date.now() }
169+
),
170+
diffLeft: (left) => post({ left, timestamp: Date.now() }),
171+
diffRight: (right) => post({ right, timestamp: Date.now() }),
172+
diffPush: (push) => post({ push, timestamp: Date.now() }),
125173
});
126174

127-
console.log(
128-
'%cJSDiff', `
175+
console.debug(
176+
'%c✚ console.diff()',
177+
`
129178
font-weight: 700;
130179
color: #000;
131180
background-color: yellow;
132181
padding: 2px 4px;
133182
border: 1px solid #bbb;
134183
border-radius: 4px;
135-
`,
136-
`console.diff(left, right); console.diffLeft(left); console.diffRight(right); console.diffPush(next);`
184+
`
137185
);
138186

139187
return true;
140188
}
141-
142-
function sendMessage(message) {
143-
return function() {
144-
chrome.runtime.sendMessage(message);
145-
};
146-
}

‎src/js/jsdiff-panel.js

-2
This file was deleted.

‎src/js/jsdiff-proxy.js

+11-9
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
;(function(w, x) {
2-
'use strict';
3-
w.addEventListener('message', function jsdiff_devtools_extension_proxy(e) {
4-
if (// message from the same frame
5-
e.source === w &&
6-
e.data && typeof(e.data) === 'object' &&
7-
e.data.source === 'jsdiff-devtools-extension-api'
1+
(() => {
2+
window.addEventListener('message', (e) => {
3+
if (
4+
typeof e.data === 'object' &&
5+
e.data !== null &&
6+
e.data.source === 'jsdiff-console-to-proxy'
87
) {
9-
x && x.sendMessage(e.data);
8+
chrome.runtime.sendMessage({
9+
source: 'jsdiff-proxy-to-devtools',
10+
payload: e.data.payload,
11+
});
1012
}
1113
});
12-
})(window, chrome.runtime);
14+
})();

‎src/js/view/api/formatter-dom.ts

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
function getElementText(_ref: HTMLElement | null): string {
2+
return _ref ? _ref.textContent || _ref.innerText : '';
3+
}
4+
5+
function eachByQuery(
6+
el: HTMLElement,
7+
query: string,
8+
fn: (_ref: HTMLElement) => void
9+
) {
10+
const elems = el.querySelectorAll(query) as NodeListOf<HTMLElement>;
11+
for (let i = 0, l = elems.length; i < l; i++) {
12+
fn(elems[i]);
13+
}
14+
}
15+
16+
function eachChildren(
17+
_ref2: ParentNode | null,
18+
fn: (el: HTMLElement, i: number) => void
19+
) {
20+
if (_ref2) {
21+
const children = _ref2.children;
22+
23+
for (let i = 0, l = children.length; i < l; i++) {
24+
fn(children[i] as HTMLElement, i);
25+
}
26+
}
27+
}
28+
29+
export const postDiffRender = (nodeArg: HTMLElement | null) => {
30+
setTimeout(function jsondiffpatchHtmlFormatterAdjustArrows() {
31+
const node = nodeArg || document;
32+
33+
eachByQuery(<HTMLElement>node, '.jsondiffpatch-arrow', function (_ref3) {
34+
const parentNode = _ref3.parentNode;
35+
const children = _ref3.children;
36+
const style = _ref3.style;
37+
const arrowParent = parentNode;
38+
const svg = children[0] as HTMLElement;
39+
const path = svg.children[1];
40+
41+
svg.style.display = 'none';
42+
43+
if (arrowParent instanceof HTMLElement) {
44+
const destination = getElementText(
45+
arrowParent.querySelector('.jsondiffpatch-moved-destination')
46+
);
47+
const container = arrowParent.parentNode;
48+
let destinationElem: unknown;
49+
50+
eachChildren(container, function (child) {
51+
if (child.getAttribute('data-key') === destination) {
52+
destinationElem = child;
53+
}
54+
});
55+
56+
if (destinationElem instanceof HTMLElement) {
57+
try {
58+
const distance = destinationElem.offsetTop - arrowParent.offsetTop;
59+
svg.setAttribute('height', `${Math.abs(distance) + 6}`);
60+
style.top = -8 + (distance > 0 ? 0 : distance) + 'px';
61+
const curve =
62+
distance > 0
63+
? 'M30,0 Q-10,' +
64+
Math.round(distance / 2) +
65+
' 26,' +
66+
(distance - 4)
67+
: 'M30,' +
68+
-distance +
69+
' Q-10,' +
70+
Math.round(-distance / 2) +
71+
' 26,4';
72+
path.setAttribute('d', curve);
73+
svg.style.display = '';
74+
} catch (ignore) {}
75+
}
76+
}
77+
});
78+
}, 10);
79+
};

‎src/js/view/api/search.ts

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
type TSearchCommands = 'performSearch' | 'nextSearchResult' | 'cancelSearch';
2+
3+
export interface ISearchOptions {
4+
cmd: TSearchCommands;
5+
query: string | null;
6+
}
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+
},
24+
};
25+
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
55+
const isHidden =
56+
container.closest('.jsondiffpatch-unchanged-hidden') &&
57+
container.closest('.jsondiffpatch-unchanged');
58+
59+
if (found && !isHidden) {
60+
container.classList.add('jsdiff-found');
61+
foundEls.push(container);
62+
}
63+
} else {
64+
for (let n = 0, N = containerNodes.length; n < N; n++) {
65+
const child = containerNodes[n];
66+
67+
if (child.nodeType === Node.ELEMENT_NODE) {
68+
highlightElements(child, query, foundEls); // recursion
69+
}
70+
}
71+
}
72+
}
73+
}
74+
75+
function clearHighlight(container: HTMLElement): void {
76+
const allFound = container.querySelectorAll(`.${CLASS_FOUND}`);
77+
78+
for (let n = allFound.length - 1; n >= 0; n--) {
79+
allFound[n].classList.remove(CLASS_FOUND, CLASS_FOUND_THIS);
80+
}
81+
}
82+
83+
function highlightCurrentResult(searchState: ISearchState): void {
84+
searchState.currentIndex++;
85+
searchState.currentIndex %= searchState.foundEls.length;
86+
87+
const el = searchState.foundEls[searchState.currentIndex];
88+
el.classList.add(CLASS_FOUND_THIS);
89+
el.scrollIntoView({
90+
behavior: 'smooth',
91+
block: 'nearest',
92+
inline: 'nearest',
93+
});
94+
}
95+
96+
export function searchQueryInDom(
97+
el: HTMLElement,
98+
{ cmd, query }: ISearchOptions
99+
): void {
100+
query = (typeof query === 'string' && query.trim()) || '';
101+
102+
// console.log('🔦', cmd, query);
103+
104+
if ('performSearch' === cmd) {
105+
searchState.query = query;
106+
107+
if (!query) {
108+
searchState.clear();
109+
clearHighlight(el);
110+
}
111+
} else if ('nextSearchResult' === cmd && searchState.query) {
112+
clearHighlight(el);
113+
searchState.foundEls.splice(0);
114+
highlightElements(el, searchState.query, searchState.foundEls);
115+
116+
if (searchState.foundEls.length) {
117+
highlightCurrentResult(searchState);
118+
} else {
119+
searchState.clear();
120+
clearHighlight(el);
121+
}
122+
} else if ('cancelSearch' === cmd) {
123+
searchState.clear();
124+
clearHighlight(el);
125+
}
126+
}

‎src/js/view/api/time.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export const SECOND = 1000;
2+
export const MINUTE = 60 * SECOND;
3+
export const HOUR = 60 * MINUTE;
4+
export const DAY = 24 * HOUR;
5+
export const WEEK = 7 * DAY;
6+
export const MONTH = 30 * DAY;
7+
export const YEAR = 365 * DAY;
8+
const intervals = [
9+
{ ge: YEAR, divisor: YEAR, unit: 'year' },
10+
{ ge: MONTH, divisor: MONTH, unit: 'month' },
11+
{ ge: WEEK, divisor: WEEK, unit: 'week' },
12+
{ ge: DAY, divisor: DAY, unit: 'day' },
13+
{ ge: HOUR, divisor: HOUR, unit: 'hour' },
14+
{ ge: MINUTE, divisor: MINUTE, unit: 'minute' },
15+
{ ge: SECOND, divisor: SECOND, unit: 'seconds' },
16+
{ ge: 0, divisor: 1, text: 'now' },
17+
];
18+
const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
19+
20+
/**
21+
* @param test {number}
22+
* @param [now] {number}
23+
* @return {string}
24+
*/
25+
export function timeFromNow(test: number, now: number): string {
26+
const delta = now - test;
27+
const absDelta = Math.abs(delta);
28+
let rv = '';
29+
30+
for (const interval of intervals) {
31+
if (absDelta >= interval.ge) {
32+
const time = Math.trunc(delta / interval.divisor);
33+
rv = interval.unit
34+
? rtf.format(-time, interval.unit as Intl.RelativeTimeFormatUnit)
35+
: interval.text || '';
36+
break;
37+
}
38+
}
39+
40+
return rv;
41+
}

‎src/js/view/api/toolkit.ts

+3
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

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { createApp } from 'vue';
2+
import Panel from './panel.vue';
3+
4+
const app = createApp(Panel);
5+
app.mount('#jsdiff-app');

‎src/js/view/panel.vue

+261-230
Large diffs are not rendered by default.

‎src/jsdiff-devtools.html

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
1-
<meta charset="utf-8"/>
2-
<script src="/src/js/jsdiff-devtools.js"></script>
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8" />
5+
<script src="/src/js/jsdiff-devtools.js"></script>
6+
</head>
7+
<body></body>
8+
</html>

‎src/jsdiff-panel.html

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<!DOCTYPE html>
22
<html>
33
<head>
4-
<meta charset="UTF-8"/>
4+
<meta charset="UTF-8" />
55
</head>
66
<body>
7-
<div id="app"></div>
8-
<script src="/src/js/bundle/jsdiff-panel.js"></script>
7+
<div id="jsdiff-app"></div>
8+
<script src="/src/js/bundle/jsdiff-panel.js"></script>
99
</body>
1010
</html>

‎tsconfig.json

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2021",
4+
"outDir": "./.ts-built",
5+
"noEmit": false,
6+
"allowJs": true,
7+
"checkJs": false,
8+
"strict": true,
9+
"skipLibCheck": true,
10+
"moduleResolution": "Node",
11+
"useDefineForClassFields": true,
12+
"module": "ESNext",
13+
"lib": ["ESNext", "DOM"],
14+
"resolveJsonModule": true,
15+
"allowSyntheticDefaultImports": true,
16+
"esModuleInterop": true,
17+
"typeRoots": ["node_modules/@types"],
18+
},
19+
"exclude": [".ts-built", "src/js/bundle"]
20+
}

‎webpack.config.js

+74-57
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,76 @@
1-
const path = require('path');
2-
const webpack = require('webpack');
3-
const CleanWebpackPlugin = require('clean-webpack-plugin');
4-
const VueLoaderPlugin = require('vue-loader/lib/plugin');
5-
6-
module.exports = {
7-
mode: 'development',
8-
9-
entry: {
10-
'jsdiff-panel': './src/js/jsdiff-panel.js'
11-
},
12-
13-
output: {
14-
filename: '[name].js',
15-
path: path.resolve(__dirname, 'src/js/bundle')
16-
},
17-
18-
resolve: {
19-
modules: [
20-
path.resolve(__dirname, 'src/js'),
21-
'node_modules'
1+
import path from 'path';
2+
import webpack from 'webpack';
3+
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
4+
import { VueLoaderPlugin } from 'vue-loader';
5+
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
6+
import { fileURLToPath } from 'url';
7+
8+
const __filename = fileURLToPath(import.meta.url);
9+
const __dirname = path.dirname(__filename);
10+
11+
export default function (env, op) {
12+
console.log('⌥', env, op.mode);
13+
const isProd = op.mode === 'production';
14+
15+
return {
16+
mode: op.mode,
17+
devServer: {
18+
hot: true,
19+
},
20+
21+
entry: {
22+
'jsdiff-panel': './src/js/view/app.js',
23+
},
24+
25+
output: {
26+
filename: '[name].js',
27+
path: path.resolve(__dirname, 'src/js/bundle'),
28+
},
29+
30+
resolve: {
31+
extensions: ['.ts', '.js'],
32+
modules: [path.resolve(__dirname, 'src/js'), 'node_modules'],
33+
alias: {},
34+
},
35+
36+
plugins: [
37+
new CleanWebpackPlugin(),
38+
new VueLoaderPlugin(),
39+
// http://127.0.0.1:8888
40+
!isProd
41+
? new BundleAnalyzerPlugin({
42+
openAnalyzer: false,
43+
logLevel: 'silent',
44+
})
45+
: () => {},
46+
new webpack.DefinePlugin({
47+
__VUE_OPTIONS_API__: 'true',
48+
__VUE_PROD_DEVTOOLS__: 'false',
49+
}),
2250
],
2351

24-
alias: {}
25-
},
26-
27-
plugins: [
28-
new webpack.IgnorePlugin({
29-
resourceRegExp: /^\.\/locale$/,
30-
contextRegExp: /moment$/
31-
}),
32-
new CleanWebpackPlugin(['src/js/bundle']),
33-
new VueLoaderPlugin()
34-
],
35-
36-
module: {
37-
rules: [
38-
{
39-
test: /\.vue$/,
40-
loader: 'vue-loader'
41-
},
42-
{
43-
test: /\.(scss|css)$/,
44-
use: [
45-
'style-loader',
46-
'css-loader',
47-
'sass-loader'
48-
]
49-
}
50-
]
51-
},
52-
53-
optimization : {
54-
runtimeChunk : false,
55-
},
56-
57-
// devtool: 'source-map'
58-
devtool: false
59-
};
52+
module: {
53+
rules: [
54+
{
55+
test: /\.tsx?$/,
56+
loader: 'ts-loader',
57+
options: { appendTsSuffixTo: [/\.vue$/], transpileOnly: true },
58+
},
59+
{
60+
test: /\.vue$/,
61+
loader: 'vue-loader',
62+
},
63+
{
64+
test: /\.(scss|css)$/,
65+
use: ['style-loader', 'css-loader', 'sass-loader'],
66+
},
67+
],
68+
},
69+
70+
optimization: {
71+
splitChunks: false,
72+
},
73+
74+
devtool: isProd ? false : 'source-map',
75+
};
76+
}

0 commit comments

Comments
 (0)
Please sign in to comment.