Skip to content

Commit 43b967d

Browse files
authored
Add Autoclick Behavior (#83)
Adds new autoclick behavior, which: - Attempts to click on all 'a' elements as the default selector, customizable via opts.clickSelector option. - Click on all links that are different page on same domain to trigger custom event handlers, re-query all links in case DOM is changed due to dynamic navigation / custom event handler, store previously seen elements. - Use heuristics to determine when link may just be a simple navigation (check event click handlers, check if target is set, check if element is visible, etc...) - Use onbeforeunload to attempt to block navigation if link navigates away - bump to 0.7.0 - Fixes #81
1 parent 3075240 commit 43b967d

File tree

7 files changed

+149
-14
lines changed

7 files changed

+149
-14
lines changed

dist/behaviors.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "browsertrix-behaviors",
3-
"version": "0.6.6",
3+
"version": "0.7.0",
44
"main": "index.js",
55
"author": "Webrecorder Software",
66
"license": "AGPL-3.0-or-later",
@@ -10,7 +10,7 @@
1010
"@webpack-cli/init": "^1.1.3",
1111
"eslint": "^7.22.0",
1212
"ts-loader": "^9.4.2",
13-
"typescript": "^4.9.5",
13+
"typescript": "^5.7.3",
1414
"webpack": "^5.75.0",
1515
"webpack-cli": "^4.5.0",
1616
"webpack-dev-server": "^3.11.2"

src/autoclick.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { BackgroundBehavior } from "./lib/behavior";
2+
import { sleep } from "./lib/utils";
3+
4+
declare let getEventListeners: any;
5+
6+
export class AutoClick extends BackgroundBehavior
7+
{
8+
_donePromise: Promise<void>;
9+
_markDone: () => void;
10+
selector: string;
11+
seenElem = new WeakSet<HTMLElement>();
12+
13+
constructor(selector = "a") {
14+
super();
15+
this.selector = selector;
16+
this._donePromise = new Promise<void>((resolve) => this._markDone = resolve);
17+
}
18+
19+
nextSameOriginLink() : HTMLAnchorElement | null {
20+
try {
21+
const allLinks = document.querySelectorAll(this.selector);
22+
for (const el of allLinks) {
23+
const elem = el as HTMLAnchorElement;
24+
25+
// skip URLs to different origin as they won't be handled dynamically, most likely just regular navigation
26+
if (elem.href && !elem.href.startsWith(self.location.origin)) {
27+
continue;
28+
}
29+
if (!elem.isConnected) {
30+
continue;
31+
}
32+
if (!elem.checkVisibility()) {
33+
continue;
34+
}
35+
if (this.seenElem.has(elem)) {
36+
continue;
37+
}
38+
this.seenElem.add(elem);
39+
return elem;
40+
}
41+
} catch (e) {
42+
this.debug(e.toString());
43+
}
44+
45+
return null;
46+
}
47+
48+
async start() {
49+
const origHref = self.location.href;
50+
51+
const beforeUnload = (event) => {
52+
event.preventDefault();
53+
return false;
54+
};
55+
56+
// process all links (except hash links) which could result in attempted navigation
57+
window.addEventListener("beforeunload", beforeUnload);
58+
59+
// process external links on current origin
60+
61+
// eslint-disable-next-line no-constant-condition
62+
while (true) {
63+
const elem = this.nextSameOriginLink();
64+
65+
if (!elem) {
66+
break;
67+
}
68+
69+
await this.processElem(elem, origHref);
70+
}
71+
72+
window.removeEventListener("beforeunload", beforeUnload);
73+
74+
this._markDone();
75+
}
76+
77+
async processElem(elem: HTMLAnchorElement, origHref: string) {
78+
// if successfully called getEventListeners and no click handler, we can skip
79+
try {
80+
if (!getEventListeners(elem).click) {
81+
return;
82+
}
83+
} catch (_e) {
84+
// getEventListeners not available, need to actually click
85+
}
86+
87+
if (elem.target) {
88+
return;
89+
}
90+
91+
const anySelf = self as any;
92+
93+
if (elem.href) {
94+
// skip if already clicked this URL, tracked in external state
95+
if (anySelf.__bx_addSet && !await anySelf.__bx_addSet(elem.href)) {
96+
return;
97+
}
98+
99+
this.debug("Clicking on link: " + elem.href);
100+
} else {
101+
this.debug("Click empty link");
102+
}
103+
104+
elem.click();
105+
106+
await sleep(250);
107+
108+
if (self.location.href != origHref) {
109+
await new Promise((resolve) => {
110+
window.addEventListener("popstate", () => {
111+
resolve(null);
112+
}, { once: true });
113+
114+
window.history.back();
115+
});
116+
}
117+
} catch (e) {
118+
this.debug(e.toString());
119+
}
120+
121+
done() {
122+
return this._donePromise;
123+
}
124+
}

src/autoscroll.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { type AutoFetcher } from "./autofetcher";
77
export class AutoScroll extends Behavior {
88
autoFetcher: AutoFetcher;
99
showMoreQuery: string;
10-
state: { segments: number };
10+
state: { segments: number } = { segments: 1};
1111
lastScrollPos: number;
1212
samePosCount: number;
1313

@@ -20,10 +20,6 @@ export class AutoScroll extends Behavior {
2020

2121
this.showMoreQuery = "//*[contains(text(), 'show more') or contains(text(), 'Show more')]";
2222

23-
this.state = {
24-
segments: 1
25-
};
26-
2723
this.lastScrollPos = -1;
2824
this.samePosCount = 0;
2925

src/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { AutoFetcher } from "./autofetcher";
22
import { Autoplay } from "./autoplay";
33
import { AutoScroll } from "./autoscroll";
4+
import { AutoClick } from "./autoclick";
45
import { awaitLoad, sleep, behaviorLog, _setLogFunc, _setBehaviorManager, installBehaviors } from "./lib/utils";
56
import { Behavior, BehaviorRunner } from "./lib/behavior";
67

@@ -15,14 +16,18 @@ interface BehaviorManagerOpts {
1516
autofetch?: boolean;
1617
autoplay?: boolean;
1718
autoscroll?: boolean;
19+
autoclick?: boolean;
1820
log?: ((...message: string[]) => void) | string | false;
1921
siteSpecific?: boolean | object;
2022
timeout?: number;
2123
fetchHeaders?: object | null;
2224
startEarly?: boolean | null;
25+
clickSelector?: string;
2326
}
2427

25-
const DEFAULT_OPTS: BehaviorManagerOpts = {autofetch: true, autoplay: true, autoscroll: true, siteSpecific: true};
28+
const DEFAULT_OPTS: BehaviorManagerOpts = {autofetch: true, autoplay: true, autoscroll: true, autoclick: true, siteSpecific: true};
29+
30+
const DEFAULT_SELECTOR = "a";
2631

2732
export class BehaviorManager {
2833
autofetch: AutoFetcher;
@@ -89,6 +94,11 @@ export class BehaviorManager {
8994
this.behaviors.push(new Autoplay(this.autofetch, opts.startEarly));
9095
}
9196

97+
if (opts.autoclick) {
98+
behaviorLog("Using AutoClick");
99+
this.behaviors.push(new AutoClick(opts.clickSelector || DEFAULT_SELECTOR));
100+
}
101+
92102
if (!this.isInTopFrame()) {
93103
return;
94104
}

tsconfig.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
"preserveConstEnums": true,
77
"allowJs": true,
88
"checkJs": true,
9-
"target": "es2020",
9+
"target": "es2022",
10+
"lib": [
11+
"es2022",
12+
"dom",
13+
"dom.iterable"
14+
],
1015
"outDir": "./dist/"
1116
},
1217
"files": [

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5237,10 +5237,10 @@ type-is@~1.6.17, type-is@~1.6.18:
52375237
media-typer "0.3.0"
52385238
mime-types "~2.1.24"
52395239

5240-
typescript@^4.9.5:
5241-
version "4.9.5"
5242-
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
5243-
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
5240+
typescript@^5.7.3:
5241+
version "5.7.3"
5242+
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e"
5243+
integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==
52445244

52455245
union-value@^1.0.0:
52465246
version "1.0.1"

0 commit comments

Comments
 (0)