Skip to content

Commit

Permalink
Add Autoclick Behavior (#83)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ikreymer authored Jan 15, 2025
1 parent 3075240 commit 43b967d
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 14 deletions.
2 changes: 1 addition & 1 deletion dist/behaviors.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "browsertrix-behaviors",
"version": "0.6.6",
"version": "0.7.0",
"main": "index.js",
"author": "Webrecorder Software",
"license": "AGPL-3.0-or-later",
Expand All @@ -10,7 +10,7 @@
"@webpack-cli/init": "^1.1.3",
"eslint": "^7.22.0",
"ts-loader": "^9.4.2",
"typescript": "^4.9.5",
"typescript": "^5.7.3",
"webpack": "^5.75.0",
"webpack-cli": "^4.5.0",
"webpack-dev-server": "^3.11.2"
Expand Down
124 changes: 124 additions & 0 deletions src/autoclick.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { BackgroundBehavior } from "./lib/behavior";
import { sleep } from "./lib/utils";

declare let getEventListeners: any;

export class AutoClick extends BackgroundBehavior
{
_donePromise: Promise<void>;
_markDone: () => void;
selector: string;
seenElem = new WeakSet<HTMLElement>();

constructor(selector = "a") {
super();
this.selector = selector;
this._donePromise = new Promise<void>((resolve) => this._markDone = resolve);
}

nextSameOriginLink() : HTMLAnchorElement | null {
try {
const allLinks = document.querySelectorAll(this.selector);
for (const el of allLinks) {
const elem = el as HTMLAnchorElement;

// skip URLs to different origin as they won't be handled dynamically, most likely just regular navigation
if (elem.href && !elem.href.startsWith(self.location.origin)) {
continue;
}
if (!elem.isConnected) {
continue;
}
if (!elem.checkVisibility()) {
continue;
}
if (this.seenElem.has(elem)) {
continue;
}
this.seenElem.add(elem);
return elem;
}
} catch (e) {
this.debug(e.toString());
}

return null;
}

async start() {
const origHref = self.location.href;

const beforeUnload = (event) => {
event.preventDefault();
return false;
};

// process all links (except hash links) which could result in attempted navigation
window.addEventListener("beforeunload", beforeUnload);

// process external links on current origin

// eslint-disable-next-line no-constant-condition
while (true) {
const elem = this.nextSameOriginLink();

if (!elem) {
break;
}

await this.processElem(elem, origHref);
}

window.removeEventListener("beforeunload", beforeUnload);

this._markDone();
}

async processElem(elem: HTMLAnchorElement, origHref: string) {
// if successfully called getEventListeners and no click handler, we can skip
try {
if (!getEventListeners(elem).click) {
return;
}
} catch (_e) {
// getEventListeners not available, need to actually click
}

if (elem.target) {
return;
}

const anySelf = self as any;

if (elem.href) {
// skip if already clicked this URL, tracked in external state
if (anySelf.__bx_addSet && !await anySelf.__bx_addSet(elem.href)) {
return;
}

this.debug("Clicking on link: " + elem.href);
} else {
this.debug("Click empty link");
}

elem.click();

await sleep(250);

if (self.location.href != origHref) {
await new Promise((resolve) => {
window.addEventListener("popstate", () => {
resolve(null);
}, { once: true });

window.history.back();
});
}
} catch (e) {
this.debug(e.toString());
}

done() {
return this._donePromise;
}
}
6 changes: 1 addition & 5 deletions src/autoscroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { type AutoFetcher } from "./autofetcher";
export class AutoScroll extends Behavior {
autoFetcher: AutoFetcher;
showMoreQuery: string;
state: { segments: number };
state: { segments: number } = { segments: 1};
lastScrollPos: number;
samePosCount: number;

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

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

this.state = {
segments: 1
};

this.lastScrollPos = -1;
this.samePosCount = 0;

Expand Down
12 changes: 11 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AutoFetcher } from "./autofetcher";
import { Autoplay } from "./autoplay";
import { AutoScroll } from "./autoscroll";
import { AutoClick } from "./autoclick";
import { awaitLoad, sleep, behaviorLog, _setLogFunc, _setBehaviorManager, installBehaviors } from "./lib/utils";
import { Behavior, BehaviorRunner } from "./lib/behavior";

Expand All @@ -15,14 +16,18 @@ interface BehaviorManagerOpts {
autofetch?: boolean;
autoplay?: boolean;
autoscroll?: boolean;
autoclick?: boolean;
log?: ((...message: string[]) => void) | string | false;
siteSpecific?: boolean | object;
timeout?: number;
fetchHeaders?: object | null;
startEarly?: boolean | null;
clickSelector?: string;
}

const DEFAULT_OPTS: BehaviorManagerOpts = {autofetch: true, autoplay: true, autoscroll: true, siteSpecific: true};
const DEFAULT_OPTS: BehaviorManagerOpts = {autofetch: true, autoplay: true, autoscroll: true, autoclick: true, siteSpecific: true};

const DEFAULT_SELECTOR = "a";

export class BehaviorManager {
autofetch: AutoFetcher;
Expand Down Expand Up @@ -89,6 +94,11 @@ export class BehaviorManager {
this.behaviors.push(new Autoplay(this.autofetch, opts.startEarly));
}

if (opts.autoclick) {
behaviorLog("Using AutoClick");
this.behaviors.push(new AutoClick(opts.clickSelector || DEFAULT_SELECTOR));
}

if (!this.isInTopFrame()) {
return;
}
Expand Down
7 changes: 6 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
"preserveConstEnums": true,
"allowJs": true,
"checkJs": true,
"target": "es2020",
"target": "es2022",
"lib": [
"es2022",
"dom",
"dom.iterable"
],
"outDir": "./dist/"
},
"files": [
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5237,10 +5237,10 @@ type-is@~1.6.17, type-is@~1.6.18:
media-typer "0.3.0"
mime-types "~2.1.24"

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

union-value@^1.0.0:
version "1.0.1"
Expand Down

0 comments on commit 43b967d

Please sign in to comment.