fix(js): implement Element scrollBy/scrollTo/scroll and functional scrollTop/scrollLeft (#431)
Element.prototype.scrollBy, scrollTo and scroll were undefined; they only existed as no-ops on the window object. Scroll-driven infinite-scroll code therefore threw "scrollBy is not a function", and scrollTop/scrollLeft were no-op stubs that always read back 0. Track a per-element scroll offset so scrollTop/scrollLeft round-trip, add the three methods accepting either (x, y) or a ScrollToOptions object, and fire a scroll event so listeners that drive lazy loading can advance. There is no layout engine, so the offset is not clamped to a real overflow maximum. Register the new methods in the native-function list so their toString stays masked. Fixes #429. Co-authored-by: sycatle <[email protected]>
This commit is contained in:
1 parent
c3ae054dde
commit
29e20ae970
1 file changed
+33
-2
Vendored
+33
-2
@@ -2094,8 +2094,13 @@ class Element extends Node {
|
|||||||
const t = this.tagName;
|
const t = this.tagName;
|
||||||
return t === 'HTML' || t === 'BODY';
|
return t === 'HTML' || t === 'BODY';
|
||||||
}
|
}
|
||||||
get scrollTop() { return 0; } set scrollTop(v) {}
|
// No layout engine, so there is no real overflow to scroll. We still track a
|
||||||
get scrollLeft() { return 0; } set scrollLeft(v) {}
|
// scroll offset so scrollTop/scrollLeft round-trip and the scroll methods
|
||||||
|
// below can report a position, which is what infinite-scroll code reads back.
|
||||||
|
get scrollTop() { return this._scrollTop || 0; }
|
||||||
|
set scrollTop(v) { v = +v; this._scrollTop = Number.isFinite(v) && v > 0 ? v : 0; }
|
||||||
|
get scrollLeft() { return this._scrollLeft || 0; }
|
||||||
|
set scrollLeft(v) { v = +v; this._scrollLeft = Number.isFinite(v) && v > 0 ? v : 0; }
|
||||||
getBoundingClientRect() {
|
getBoundingClientRect() {
|
||||||
globalThis.__obscura_click_target = this;
|
globalThis.__obscura_click_target = this;
|
||||||
// documentElement and body span the full viewport. Without this every
|
// documentElement and body span the full viewport. Without this every
|
||||||
@@ -2153,6 +2158,31 @@ class Element extends Node {
|
|||||||
get ariaSelected() { return this.getAttribute('aria-selected'); }
|
get ariaSelected() { return this.getAttribute('aria-selected'); }
|
||||||
set ariaSelected(v) { if (v == null) this.removeAttribute('aria-selected'); else this.setAttribute('aria-selected', String(v)); }
|
set ariaSelected(v) { if (v == null) this.removeAttribute('aria-selected'); else this.setAttribute('aria-selected', String(v)); }
|
||||||
scrollIntoView() { globalThis.__obscura_click_target = this; }
|
scrollIntoView() { globalThis.__obscura_click_target = this; }
|
||||||
|
// scrollTo/scrollBy/scroll accept either (x, y) or a ScrollToOptions object.
|
||||||
|
// Without layout the offset cannot be clamped to a real max, but updating it
|
||||||
|
// and firing a scroll event lets scroll-driven lazy loaders advance instead
|
||||||
|
// of throwing "scrollBy is not a function".
|
||||||
|
scrollTo(x, y) {
|
||||||
|
let left, top;
|
||||||
|
if (x !== null && typeof x === 'object') { left = x.left; top = x.top; }
|
||||||
|
else { left = x; top = y; }
|
||||||
|
if (left !== undefined) this.scrollLeft = +left || 0;
|
||||||
|
if (top !== undefined) this.scrollTop = +top || 0;
|
||||||
|
this._fireScroll();
|
||||||
|
}
|
||||||
|
scroll(x, y) { this.scrollTo(x, y); }
|
||||||
|
scrollBy(x, y) {
|
||||||
|
let dl, dt;
|
||||||
|
if (x !== null && typeof x === 'object') { dl = x.left; dt = x.top; }
|
||||||
|
else { dl = x; dt = y; }
|
||||||
|
this.scrollLeft = (this.scrollLeft || 0) + (+dl || 0);
|
||||||
|
this.scrollTop = (this.scrollTop || 0) + (+dt || 0);
|
||||||
|
this._fireScroll();
|
||||||
|
}
|
||||||
|
_fireScroll() {
|
||||||
|
const self = this;
|
||||||
|
setTimeout(() => { try { self.dispatchEvent(new Event('scroll', { bubbles: false })); } catch (e) {} }, 0);
|
||||||
|
}
|
||||||
animate(keyframes, options) {
|
animate(keyframes, options) {
|
||||||
const duration = typeof options === 'number' ? options : (options?.duration || 0);
|
const duration = typeof options === 'number' ? options : (options?.duration || 0);
|
||||||
return {
|
return {
|
||||||
@@ -5954,6 +5984,7 @@ _markNative(globalThis.Selection);
|
|||||||
Element.prototype.cloneNode, Element.prototype.attachShadow,
|
Element.prototype.cloneNode, Element.prototype.attachShadow,
|
||||||
Element.prototype.insertAdjacentHTML, Element.prototype.insertAdjacentText,
|
Element.prototype.insertAdjacentHTML, Element.prototype.insertAdjacentText,
|
||||||
Element.prototype.insertAdjacentElement, Element.prototype.scrollIntoView,
|
Element.prototype.insertAdjacentElement, Element.prototype.scrollIntoView,
|
||||||
|
Element.prototype.scrollTo, Element.prototype.scrollBy, Element.prototype.scroll,
|
||||||
Element.prototype.append, Element.prototype.prepend, Element.prototype.remove,
|
Element.prototype.append, Element.prototype.prepend, Element.prototype.remove,
|
||||||
Element.prototype.before, Element.prototype.after, Element.prototype.replaceWith,
|
Element.prototype.before, Element.prototype.after, Element.prototype.replaceWith,
|
||||||
HTMLFormElement.prototype.reset,
|
HTMLFormElement.prototype.reset,
|
||||||
|
|||||||
Reference in new issue
Block a user