Mon 21 Jul 22:43:21 CEST 2025

This commit is contained in:
sbosse 2025-07-21 23:11:17 +02:00
parent 41a6dec074
commit c37d7f01f9

View File

@ -0,0 +1,446 @@
/**
** ==============================
** O O O OOOO
** O O O O O O
** O O O O O O
** OOOO OOOO O OOO OOOO
** O O O O O O O
** O O O O O O O
** OOOO OOOO O O OOOO
** ==============================
** Dr. Stefan Bosse http://www.bsslab.de
**
** COPYRIGHT: THIS SOFTWARE, EXECUTABLE AND SOURCE CODE IS OWNED
** BY THE AUTHOR(S).
** THIS SOURCE CODE MAY NOT BE COPIED, EXTRACTED,
** MODIFIED, OR OTHERWISE USED IN A CONTEXT
** OUTSIDE OF THE SOFTWARE SYSTEM.
**
** $AUTHORS: Christopher Jeffrey and contributors, Stefan Bosse
** $INITIAL: (C) 2013-2016, Christopher Jeffrey and contributors
** $MODIFIED: sbosse (2017-2021).
** $VERSION: 1.2.3
**
** $INFO:
*
* scrollablebox.js - scrollable box element for blessed
*
* events: 'mouse', 'click' (low level), 'clicked' (high level)
*
** $ENDOFINFO
*/
/**
* Modules
*/
var Comp = Require('com/compat');
var Node = Require('term/widgets/node');
var Box = Require('term/widgets/box');
/**
* ScrollableBox
*/
function ScrollableBox(options) {
var self = this;
if (!instanceOf(this,Node)) {
return new ScrollableBox(options);
}
options = options || {};
Box.call(this, options);
if (options.scrollable === false) {
return this;
}
this.scrollable = true;
this.childOffset = 0;
this.childBase = 0;
this.baseLimit = options.baseLimit || Infinity;
this.alwaysScroll = options.alwaysScroll;
this.scrollbar = options.scrollbar;
if (this.scrollbar) {
this.scrollbar.ch = this.scrollbar.ch || ' ';
this.style.scrollbar = this.style.scrollbar || this.scrollbar.style;
if (!this.style.scrollbar) {
this.style.scrollbar = {};
this.style.scrollbar.fg = this.scrollbar.fg;
this.style.scrollbar.bg = this.scrollbar.bg;
this.style.scrollbar.bold = this.scrollbar.bold;
this.style.scrollbar.underline = this.scrollbar.underline;
this.style.scrollbar.inverse = this.scrollbar.inverse;
this.style.scrollbar.invisible = this.scrollbar.invisible;
}
//this.scrollbar.style = this.style.scrollbar;
if (this.track || this.scrollbar.track) {
this.track = this.scrollbar.track || this.track;
this.style.track = this.style.scrollbar.track || this.style.track;
this.track.ch = this.track.ch || ' ';
this.style.track = this.style.track || this.track.style;
if (!this.style.track) {
this.style.track = {};
this.style.track.fg = this.track.fg;
this.style.track.bg = this.track.bg;
this.style.track.bold = this.track.bold;
this.style.track.underline = this.track.underline;
this.style.track.inverse = this.track.inverse;
this.style.track.invisible = this.track.invisible;
}
this.track.style = this.style.track;
}
// Allow controlling of the scrollbar via the mouse:
if (options.mouse) {
this.on('mousedown', function(data) {
if (self._scrollingBar) {
// Do not allow dragging on the scrollbar:
delete self.screen._dragging;
delete self._drag;
return;
}
var x = data.x - self.aleft;
var y = data.y - self.atop;
self.emit('clicked',{x:x-1,y:y-1,ev:'click'});
if (x === self.width - self.iright - 1) {
// Do not allow dragging on the scrollbar:
delete self.screen._dragging;
delete self._drag;
var perc = (y - self.itop) / (self.height - self.iheight);
self.setScrollPerc(perc * 100 | 0);
self.screen.render();
var smd, smu;
self._scrollingBar = true;
self.onScreenEvent('mousedown', smd = function(data) {
var y = data.y - self.atop;
var perc = y / self.height;
self.setScrollPerc(perc * 100 | 0);
self.screen.render();
});
// If mouseup occurs out of the window, no mouseup event fires, and
// scrollbar will drag again on mousedown until another mouseup
// occurs.
self.onScreenEvent('mouseup', smu = function() {
self._scrollingBar = false;
self.removeScreenEvent('mousedown', smd);
self.removeScreenEvent('mouseup', smu);
});
}
});
}
}
if (options.mouse) {
this.on('wheeldown', function() {
self.scroll(self.height / 2 | 0 || 1);
self.screen.render();
});
this.on('wheelup', function() {
self.scroll(-(self.height / 2 | 0) || -1);
self.screen.render();
});
}
if (options.keys && !options.ignoreKeys) {
this.on('keypress', function(ch, key) {
if (key.name === 'up' || (options.vi && key.name === 'k')) {
self.scroll(-1);
self.screen.render();
return;
}
if (key.name === 'down' || (options.vi && key.name === 'j')) {
self.scroll(1);
self.screen.render();
return;
}
if (key.name === 'pageup') {
self.scroll(-(self.height / 2 | 0) || -1);
self.screen.render();
return;
}
if (key.name === 'pagedown') {
self.scroll(self.height / 2 | 0 || 1);
self.screen.render();
return;
}
if (options.vi && key.name === 'u' && key.ctrl) {
self.scroll(-(self.height / 2 | 0) || -1);
self.screen.render();
return;
}
if (options.vi && key.name === 'd' && key.ctrl) {
self.scroll(self.height / 2 | 0 || 1);
self.screen.render();
return;
}
if (options.vi && key.name === 'b' && key.ctrl) {
self.scroll(-self.height || -1);
self.screen.render();
return;
}
if (options.vi && key.name === 'f' && key.ctrl) {
self.scroll(self.height || 1);
self.screen.render();
return;
}
if (options.vi && key.name === 'g' && !key.shift) {
self.scrollTo(0);
self.screen.render();
return;
}
if (options.vi && key.name === 'g' && key.shift) {
self.scrollTo(self.getScrollHeight());
self.screen.render();
return;
}
});
}
this.on('parsed content', function() {
self._recalculateIndex();
});
self._recalculateIndex();
}
//ScrollableBox.prototype.__proto__ = Box.prototype;
inheritPrototype(ScrollableBox,Box);
ScrollableBox.prototype.type = 'scrollable-box';
/* depricated
// XXX Potentially use this in place of scrollable checks elsewhere.
ScrollableBox.prototype.__defineGetter__('reallyScrollable', function() {
if (this.shrink) return this.scrollable;
return this.getScrollHeight() > this.height;
});
*/
Object.defineProperty(ScrollableBox.prototype,'reallyScrollable',{
get: function () {
if (this.shrink) return this.scrollable;
return this.getScrollHeight() > this.height;
},
set: function (val) {
}
});
ScrollableBox.prototype._scrollBottom = function() {
if (!this.scrollable) return 0;
// We could just calculate the children, but we can
// optimize for lists by just returning the items.length.
if (this._isList) {
return this.items ? this.items.length : 0;
}
if (this.lpos && this.lpos._scrollBottom) {
return this.lpos._scrollBottom;
}
var bottom = this.children.reduce(function(current, el) {
// el.height alone does not calculate the shrunken height, we need to use
// getCoords. A shrunken box inside a scrollable element will not grow any
// larger than the scrollable element's context regardless of how much
// content is in the shrunken box, unless we do this (call getCoords
// without the scrollable calculation):
// See: $ node test/widget-shrink-fail-2.js
if (!el.detached) {
var lpos = el._getCoords(false, true);
if (lpos) {
return Math.max(current, el.rtop + (lpos.yl - lpos.yi));
}
}
return Math.max(current, el.rtop + el.height);
}, 0);
// XXX Use this? Makes .getScrollHeight() useless!
// if (bottom < this._clines.length) bottom = this._clines.length;
if (this.lpos) this.lpos._scrollBottom = bottom;
return bottom;
};
ScrollableBox.prototype.setScroll =
ScrollableBox.prototype.scrollTo = function(offset, always) {
// XXX
// At first, this appeared to account for the first new calculation of childBase:
this.scroll(0);
return this.scroll(offset - (this.childBase + this.childOffset), always);
};
ScrollableBox.prototype.getScroll = function() {
return this.childBase + this.childOffset;
};
ScrollableBox.prototype.scroll = function(offset, always) {
if (!this.scrollable) return;
if (this.detached) return;
// Handle scrolling.
var visible = this.height - this.iheight
, base = this.childBase
, d
, p
, t
, b
, max
, emax;
if (this.alwaysScroll || always) {
// Semi-workaround
this.childOffset = offset > 0
? visible - 1 + offset
: offset;
} else {
this.childOffset += offset;
}
if (this.childOffset > visible - 1) {
d = this.childOffset - (visible - 1);
this.childOffset -= d;
this.childBase += d;
} else if (this.childOffset < 0) {
d = this.childOffset;
this.childOffset += -d;
this.childBase += d;
}
if (this.childBase < 0) {
this.childBase = 0;
} else if (this.childBase > this.baseLimit) {
this.childBase = this.baseLimit;
}
// Find max "bottom" value for
// content and descendant elements.
// Scroll the content if necessary.
if (this.childBase === base) {
return this.emit('scroll');
}
// When scrolling text, we want to be able to handle SGR codes as well as line
// feeds. This allows us to take preformatted text output from other programs
// and put it in a scrollable text box.
this.parseContent();
// XXX
// max = this.getScrollHeight() - (this.height - this.iheight);
max = this._clines.length - (this.height - this.iheight);
if (max < 0) max = 0;
emax = this._scrollBottom() - (this.height - this.iheight);
if (emax < 0) emax = 0;
this.childBase = Math.min(this.childBase, Math.max(emax, max));
if (this.childBase < 0) {
this.childBase = 0;
} else if (this.childBase > this.baseLimit) {
this.childBase = this.baseLimit;
}
// Optimize scrolling with CSR + IL/DL.
p = this.lpos;
// Only really need _getCoords() if we want
// to allow nestable scrolling elements...
// or if we **really** want shrinkable
// scrolling elements.
// p = this._getCoords();
if (p && this.childBase !== base && this.screen.cleanSides(this)) {
t = p.yi + this.itop;
b = p.yl - this.ibottom - 1;
d = this.childBase - base;
if (d > 0 && d < visible) {
// scrolled down
this.screen.deleteLine(d, t, t, b);
} else if (d < 0 && -d < visible) {
// scrolled up
d = -d;
this.screen.insertLine(d, t, t, b);
}
}
return this.emit('scroll');
};
ScrollableBox.prototype.scrollBottom = function () {
// Workaround: inserting lines when scrollbar was manually set
// breaks scroll window (can't usee _scrollBottom...)
this.scrollTo(10000000);
};
ScrollableBox.prototype._recalculateIndex = function() {
var max, emax;
if (this.detached || !this.scrollable) {
return 0;
}
// XXX
// max = this.getScrollHeight() - (this.height - this.iheight);
max = this._clines.length - (this.height - this.iheight);
if (max < 0) max = 0;
emax = this._scrollBottom() - (this.height - this.iheight);
if (emax < 0) emax = 0;
this.childBase = Math.min(this.childBase, Math.max(emax, max));
if (this.childBase < 0) {
this.childBase = 0;
} else if (this.childBase > this.baseLimit) {
this.childBase = this.baseLimit;
}
};
ScrollableBox.prototype.resetScroll = function() {
if (!this.scrollable) return;
this.childOffset = 0;
this.childBase = 0;
return this.emit('scroll');
};
ScrollableBox.prototype.getScrollHeight = function() {
return Math.max(this._clines.length, this._scrollBottom());
};
ScrollableBox.prototype.getScrollPerc = function(s) {
var pos = this.lpos || this._getCoords();
if (!pos) return s ? -1 : 0;
var height = (pos.yl - pos.yi) - this.iheight
, i = this.getScrollHeight()
, p;
if (height < i) {
if (this.alwaysScroll) {
p = this.childBase / (i - height);
} else {
p = (this.childBase + this.childOffset) / (i - 1);
}
return p * 100;
}
return s ? -1 : 0;
};
ScrollableBox.prototype.setScrollPerc = function(i) {
// XXX
// var m = this.getScrollHeight();
var m = Math.max(this._clines.length, this._scrollBottom());
return this.scrollTo((i / 100) * m | 0);
};
/**
* Expose
*/
module.exports = ScrollableBox;