pvbemu/web/toolkit/ScrollPane.js

318 lines
9.9 KiB
JavaScript

let register = Toolkit => Toolkit.ScrollPane =
// Scrolling container for larger internal elements
class ScrollPane extends Toolkit.Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options = {}) {
super(app, options = Object.assign({
class: "tk scroll-pane"
}, options, { style: Object.assign({
display : "inline-grid",
gridAutoRows: "auto max-content",
overflow : "hidden",
position : "relative"
}, options.style || {}) }));
// Configure options
this._overflowX = "auto";
this._overflowY = "auto";
if ("overflowX" in options)
this.overflowX = options.overflowX;
if ("overflowY" in options)
this.overflowY = options.overflowY;
// Component
this.addEventListener("wheel", e=>this.onWheel(e));
// Viewport
this.viewport = new Toolkit.Component(app, {
class: "viewport",
style: {
overflow: "hidden"
}
});
this.viewport.element.id = Toolkit.id();
this.viewport.addEventListener("keydown", e=>this.onKeyDown (e));
this.viewport.addEventListener("resize" , e=>this.onResize ( ));
this.viewport.addEventListener("scroll" , e=>this.onInnerScroll( ));
this.viewport.addEventListener("visibility",
e=>{ if (e.visible) this.onResize(); });
this.add(this.viewport);
// View resize manager
this.viewResizer = new ResizeObserver(()=>this.onResize());
// Vertical scroll bar
this.vscroll = new Toolkit.ScrollBar(app, {
orientation: "vertical",
visibility : true
});
this.vscroll.controls = this.viewport;
this.vscroll.addEventListener("scroll",
e=>this.onOuterScroll(e, true));
this.add(this.vscroll);
// Horizontal scroll bar
this.hscroll = new Toolkit.ScrollBar(app, {
orientation: "horizontal",
visibility : true
});
this.hscroll.controls = this.viewport;
this.hscroll.addEventListener("scroll",
e=>this.onOuterScroll(e, false));
this.add(this.hscroll);
// Corner mask (for when both scroll bars are visible)
this.corner = new Toolkit.Component(app, { class: "corner" });
this.add(this.corner);
// Configure view
this.view = options.view || null;
}
///////////////////////////// Event Handlers //////////////////////////////
// Key press
onKeyDown(e) {
// Error checking
if (e.altKey || e.ctrlKey || e.shiftKey || this.disabled)
return;
// Processing by key
switch(e.key) {
case "ArrowDown":
this.viewport.element.scrollTop +=
this.vscroll.unitIncrement;
break;
case "ArrowLeft":
this.viewport.element.scrollLeft -=
this.hscroll.unitIncrement;
break;
case "ArrowRight":
this.viewport.element.scrollLeft +=
this.hscroll.unitIncrement;
break;
case "ArrowUp":
this.viewport.element.scrollTop -=
this.vscroll.unitIncrement;
break;
case "PageDown":
this.viewport.element.scrollTop +=
this.vscroll.blockIncrement;
break;
case "PageUp":
this.viewport.element.scrollTop -=
this.vscroll.blockIncrement;
break;
default: return;
}
Toolkit.handle(e);
}
// Component resized
onResize() {
// Error checking
if (!this.viewport)
return;
// Working variables
let viewport = this.viewport.element.getBoundingClientRect();
let scrollHeight =
this.hscroll.element.getBoundingClientRect().height;
let scrollWidth =
this.vscroll.element.getBoundingClientRect().width;
let viewHeight = this.viewHeight;
let viewWidth = this.viewWidth;
let fullHeight =
viewport.height + (this.hscroll.visible ? scrollHeight : 0);
let fullWidth =
viewport.width + (this.vscroll.visible ? scrollWidth : 0);
// Configure scroll bars
this.hscroll.max = viewWidth;
this.hscroll.blockIncrement = viewport.width;
this.hscroll.value = this.viewport.element.scrollLeft;
this.vscroll.max = viewHeight;
this.vscroll.blockIncrement = viewport.height;
this.vscroll.value = this.viewport.element.scrollTop;
// Determine whether the vertical scroll bar is visible
let vert = false;
if (
this.overflowY == "scroll" ||
this.overflowY == "auto" &&
viewHeight > fullHeight &&
scrollWidth <= viewWidth
) {
fullWidth -= scrollWidth;
vert = true;
}
// Determine whether the horizontal scroll bar is visible
let horz = false;
if (
this.overflowX == "scroll" ||
this.overflowX == "auto" &&
viewWidth > fullWidth &&
scrollHeight <= viewHeight
) {
fullHeight -= scrollHeight;
horz = true;
}
// The horizontal scroll bar necessitates the vertical scroll bar
vert = vert ||
this.overflowY == "auto" &&
viewHeight > fullHeight &&
scrollWidth < viewWidth
;
// Configure scroll bar visibility
this.setScrollBars(horz, vert);
}
// View scrolled
onInnerScroll() {
this.hscroll.value = this.viewport.element.scrollLeft;
this.vscroll.value = this.viewport.element.scrollTop;
}
// Scroll bar scrolled
onOuterScroll(e, vertical) {
this.viewport.element[vertical?"scrollTop":"scrollLeft"] = e.scroll;
}
// Mouse wheel scrolled
onWheel(e) {
let delta = e.deltaY;
// Error checking
if (e.altKey || e.ctrlKey || e.shiftKey || delta == 0)
return;
// Scrolling by pixel
if (e.deltaMode == WheelEvent.DOM_DELTA_PIXEL) {
let max = this.vscroll.unitIncrement * 3;
delta = Math.min(max, Math.max(-max, delta));
}
// Scrolling by line
else if (e.deltaMode == WheelEvent.DOM_DELTA_LINE) {
delta = Math[delta < 0 ? "floor" : "ceil"](delta) *
this.vscroll.unitIncrement;
}
// Scrolling by page
else if (e.deltaMode == WheelEvent.DOM_DELTA_PAGE) {
delta = Math[delta < 0 ? "floor" : "ceil"](delta) *
this.vscroll.blockIncrement;
}
this.viewport.element.scrollTop += delta;
Toolkit.handle(e);
}
///////////////////////////// Public Methods //////////////////////////////
// Horizontal scroll bar policy
get overflowX() { return this._overflowX; }
set overflowX(policy) {
switch (policy) {
case "auto":
case "hidden":
case "scroll":
this._overflowX = policy;
this.onResize();
}
}
// Vertical scroll bar policy
get overflowY() { return this._overflowY; }
set overflowY(policy) {
switch (policy) {
case "auto":
case "hidden":
case "scroll":
this._overflowY = policy;
this.onResize();
}
}
// Horizontal scrolling position
get scrollLeft() { return this.viewport.element.scrollLeft; }
set scrollLeft(left) { this.viewport.element.scrollLeft = left; }
// Vertical scrolling position
get scrollTop() { return this.viewport.element.scrollTop; }
set scrollTop(top) { this.viewport.element.scrollTop = top; }
// Represented scrollable region
get view() {
let ret = this.viewport.element.querySelector("*");
return ret && ret.component || ret || null;
}
set view(view) {
view = view instanceof Toolkit.Component ? view.element : view;
if (view == null) {
view = this.viewport.element.querySelector("*");
if (view)
this.viewResizer.unobserve(view);
this.viewport.element.replaceChildren();
} else {
this.viewport.element.replaceChildren(view);
this.viewResizer.observe(view);
}
}
// Height of the view element
set viewHeight(x) { }
get viewHeight() {
let view = this.view && this.view.element || this.view;
return Math.min(
this.viewport.element.scrollHeight,
view ? view.clientHeight : 0
);
}
// width of the view element
set viewWidth(x) { }
get viewWidth() {
let view = this.view && this.view.element || this.view;
return Math.min(
this.viewport.element.scrollWidth,
view ? view.clientWidth : 0
);
}
///////////////////////////// Private Methods /////////////////////////////
// Configure scroll bar visibility
setScrollBars(horz, vert) {
this.element.style.gridTemplateColumns =
"auto" + (vert ? " max-content" : "");
this.hscroll.visible = horz;
this.hscroll.element.style[horz ? "removeProperty" : "setProperty"]
("position", "absolute");
this.vscroll.visible = vert;
this.vscroll.element.style[vert ? "removeProperty" : "setProperty"]
("position", "absolute");
this.corner.visible = vert && horz;
this.element.classList[horz ? "add" : "remove"]("horizontal");
this.element.classList[vert ? "add" : "remove"]("vertical");
}
}
export { register };