1
1
mirror of https://github.com/gorhill/uBlock.git synced 2025-10-05 21:32:39 +02:00

Improve trusted-click-element scriptlet

Related discussion/issue:
- https://github.com/uBlockOrigin/uBlock-issues/issues/2917
- https://github.com/uBlockOrigin/uAssets/discussions/30124

The "list of selectors" parameter is now a "list of steps". A step
can be:

- A selector, which tells the scriptlet to click a matching element. If
  no matching element is found, the scriptlet will wait for a matching
  element  to become available.
- An integer, which tells the scriptlet to wait n ms before processing
  the next step
- A directive, which is a string starting with `!` (not implemented yet)

If the last item in the list is an integer, this tells the scriplet to
override the built-in timeout value of 10s, such that the life time of
the scriptlet can now be extended beyond 10s.

Example:

  ..##+js(trusted-click-element, '1000, a, 500, b, c, 15000')

The scriptlet filter above will perform the following steps, in order:

- Prepare the scriptlet to timeout at 15s from now
- Wait 1000 ms
- Wait for element `a` to become available then click on it
- Wait 500 ms
- Wait for element `b` to become available then click on it
- Wait for element `c` to become available then click on it
- Abort if all the steps cannot be completed before 15s

The changes keep compatiblity with older syntax or with AdGuard
syntax.
This commit is contained in:
Raymond Hill
2025-09-22 18:50:58 -04:00
parent 705e6329eb
commit 9aa91ba111

View File

@@ -2186,92 +2186,103 @@ function trustedClickElement(
return shadowRoot && querySelectorEx(inside, shadowRoot);
};
const selectorList = safe.String_split.call(selectors, /\s*,\s*/)
.filter(s => {
try {
void querySelectorEx(s);
} catch {
return false;
}
return true;
});
if ( selectorList.length === 0 ) { return; }
const steps = safe.String_split.call(selectors, /\s*,\s*/).map(a => {
if ( /^\d+$/.test(a) ) { return parseInt(a, 10); }
return a;
});
if ( steps.length === 0 ) { return; }
const clickDelay = parseInt(delay, 10) || 1;
const t0 = Date.now();
const tbye = t0 + 10000;
let tnext = selectorList.length !== 1 ? t0 : t0 + clickDelay;
for ( let i = steps.length-1; i > 0; i-- ) {
if ( typeof steps[i] !== 'string' ) { continue; }
if ( typeof steps[i-1] !== 'string' ) { continue; }
steps.splice(i, 0, clickDelay);
}
if ( typeof steps.at(-1) !== 'number' ) {
steps.push(10000);
}
const waitForTime = ms => {
return new Promise(resolve => {
safe.uboLog(logPrefix, `Waiting for ${ms} ms`);
waitForTime.timer = setTimeout(( ) => {
waitForTime.timer = undefined;
resolve();
}, ms);
});
};
waitForTime.cancel = ( ) => {
const { timer } = waitForTime;
if ( timer === undefined ) { return; }
clearTimeout(timer);
waitForTime.timer = undefined;
};
const waitForElement = selector => {
return new Promise(resolve => {
const elem = querySelectorEx(selector);
if ( elem !== null ) {
elem.click();
resolve();
return;
}
safe.uboLog(logPrefix, `Waiting for ${selector}`);
const observer = new MutationObserver(( ) => {
const elem = querySelectorEx(selector);
if ( elem === null ) { return; }
waitForElement.cancel();
elem.click();
resolve();
});
observer.observe(document, {
attributes: true,
childList: true,
subtree: true,
});
waitForElement.observer = observer;
});
};
waitForElement.cancel = ( ) => {
const { observer } = waitForElement;
if ( observer === undefined ) { return; }
waitForElement.observer = undefined;
observer.disconnect();
};
const waitForTimeout = ms => {
waitForTimeout.cancel();
waitForTimeout.timer = setTimeout(( ) => {
waitForTimeout.timer = undefined;
terminate();
safe.uboLog(logPrefix, `Timed out after ${ms} ms`);
}, ms);
};
waitForTimeout.cancel = ( ) => {
if ( waitForTimeout.timer === undefined ) { return; }
clearTimeout(waitForTimeout.timer);
waitForTimeout.timer = undefined;
};
const terminate = ( ) => {
selectorList.length = 0;
next.stop();
observe.stop();
waitForTime.cancel();
waitForElement.cancel();
waitForTimeout.cancel();
};
const next = notFound => {
if ( selectorList.length === 0 ) {
safe.uboLog(logPrefix, 'Completed');
return terminate();
const process = async ( ) => {
waitForTimeout(steps.pop());
while ( steps.length !== 0 ) {
const step = steps.shift();
if ( step === undefined ) { break; }
if ( typeof step === 'number' ) {
await waitForTime(step);
if ( step === 1 ) { continue; }
continue;
}
if ( step.startsWith('!') ) { continue; }
await waitForElement(step);
safe.uboLog(logPrefix, `Clicked ${step}`);
}
const tnow = Date.now();
if ( tnow >= tbye ) {
safe.uboLog(logPrefix, 'Timed out');
return terminate();
}
if ( notFound ) { observe(); }
const delay = Math.max(notFound ? tbye - tnow : tnext - tnow, 1);
next.timer = setTimeout(( ) => {
next.timer = undefined;
process();
}, delay);
safe.uboLog(logPrefix, `Waiting for ${selectorList[0]}...`);
};
next.stop = ( ) => {
if ( next.timer === undefined ) { return; }
clearTimeout(next.timer);
next.timer = undefined;
};
const observe = ( ) => {
if ( observe.observer !== undefined ) { return; }
observe.observer = new MutationObserver(( ) => {
if ( observe.timer !== undefined ) { return; }
observe.timer = setTimeout(( ) => {
observe.timer = undefined;
process();
}, 20);
});
observe.observer.observe(document, {
attributes: true,
childList: true,
subtree: true,
});
};
observe.stop = ( ) => {
if ( observe.timer !== undefined ) {
clearTimeout(observe.timer);
observe.timer = undefined;
}
if ( observe.observer ) {
observe.observer.disconnect();
observe.observer = undefined;
}
};
const process = ( ) => {
next.stop();
if ( Date.now() < tnext ) { return next(); }
const selector = selectorList.shift();
if ( selector === undefined ) { return terminate(); }
const elem = querySelectorEx(selector);
if ( elem === null ) {
selectorList.unshift(selector);
return next(true);
}
safe.uboLog(logPrefix, `Clicked ${selector}`);
elem.click();
tnext += clickDelay;
next();
terminate();
};
runAtHtmlElementFn(process);